Skip to main content

ferroui_controls_inking/
ink_canvas.rs

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/// A pressure-sensitive ink surface that renders entirely through FerroUI.
39#[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        // A transparent draw establishes the hit-test surface even when it is empty.
122        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                // FerroUI's platform-neutral timestamps are milliseconds. InkPoint
347                // stores microseconds; coalesced points share the batch timestamp.
348                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        // One nonzero-filled geometry per stroke: overlapping segments of a
400        // translucent highlighter never accumulate opacity within that stroke.
401        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}