Skip to main content

ferroui_controls_inking/
ink_toolbar_configuration.rs

1//! Pen palette and width editing built from the existing FerroUI controls.
2use super::color_names::{bind_color_name, high_contrast_colors};
3use super::ink_toolbar::round_panel_thickness;
4use super::ink_toolbar_buttons::{strings, toolbar_style_key};
5use super::{InkToolbarFlyoutItemKind, InkToolbarPenButton};
6use ferroui_controls::automation::AutomationProperties;
7use ferroui_controls::primitives::{RangeBase, TemplatedControlImpl, ToggleButton, ToggleButtonImpl};
8use ferroui_controls::shapes::{Ellipse, Path};
9use ferroui_controls::utils::BorderRenderHelper;
10use ferroui_controls::{
11    Border, ButtonImpl, Canvas, ContentControlImpl, Control, ControlImpl, Decorator, PanelImpl,
12    Slider, SliderImpl, StackPanel, StackPanelImpl, TextBlock, WrapPanel,
13};
14use ferroui_base::animation::easings::CubicEaseOut;
15use ferroui_base::animation::{Animation, Cue, FillMode, IClock, IGlobalClock, KeyFrame, TimeSpan};
16use ferroui_base::collections::FerroList;
17use ferroui_base::data::BindingPriority;
18use ferroui_base::input::{
19    InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyboardNavigation,
20    KeyboardNavigationMode, NavigationMethod,
21};
22use ferroui_base::interactivity::InteractiveImpl;
23use ferroui_base::layout::{Layoutable, LayoutableImpl, LayoutableImplExt, Orientation};
24use ferroui_base::media::immutable::ImmutableSolidColorBrush;
25use ferroui_base::media::{
26    BackgroundSizing, BoxShadows, DrawingContext, FontWeight, IBrush, ITransform, MatrixTransform,
27    PenLineCap, PenLineJoin, PolylineGeometry, TransformGroup, TranslateTransform,
28};
29use ferroui_base::reactive::{CompositeDisposable, Disposable, IDisposable, Observable};
30use ferroui_base::styling::Setter;
31use ferroui_base::utilities::HandlerList;
32use ferroui_base::{
33    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, CornerRadius,
34    DirectProperty, FerroLocator, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
35    FerroPropertyChangedEventArgs, LocatorExtensions, Point, Rect, Ref, Size, StyledElementImpl,
36    StyledProperty, Thickness, TypeInfo, Visual, VisualImpl, VisualImplExt,
37    VisualTreeAttachmentEventArgs, WeakRef,
38};
39use std::cell::{Cell, RefCell};
40use std::rc::Rc;
41
42#[repr(C)]
43pub struct InkToolbarStrokeWidthSlider {
44    base: Slider,
45}
46ferro_class!(InkToolbarStrokeWidthSlider:Slider);
47ferro_class_info!(InkToolbarStrokeWidthSlider {
48    new: InkToolbarStrokeWidthSlider::new
49});
50ferro_impl_classes!(InkToolbarStrokeWidthSlider: VisualImpl, LayoutableImpl, InteractiveImpl, InputElementImpl, TemplatedControlImpl, SliderImpl);
51impl ControlImpl for InkToolbarStrokeWidthSlider {
52    fn on_create_automation_peer(this: &Self) -> Ref<ferroui_controls::automation::peers::AutomationPeer> {
53        super::InkToolbarStrokeWidthSliderAutomationPeer::new(this).upcast()
54    }
55}
56
57impl StyledElementImpl for InkToolbarStrokeWidthSlider {
58    fn style_key_override(this: &Self) -> &'static TypeInfo {
59        toolbar_style_key(this, Slider::TYPE)
60    }
61}
62impl FerroObjectImpl for InkToolbarStrokeWidthSlider {
63    fn constructed(this: &Self) {
64        Self::parent_constructed(this);
65        this.set_small_change(0.5);
66        this.set_large_change(2.0);
67        strings::bind_name(this, "InkToolbarPenConfigurationSizeSliderName");
68    }
69}
70impl InkToolbarStrokeWidthSlider {
71    pub fn construct() -> Self {
72        Self {
73            base: Slider::construct(),
74        }
75    }
76    pub fn new() -> Ref<Self> {
77        instantiate(Self::construct())
78    }
79}
80
81#[repr(C)]
82pub struct InkToolbarPenConfigurationControl {
83    base: StackPanel,
84    // The pen's flyout owns its configuration. Keep the reverse owner link weak.
85    pen: RefCell<Option<WeakRef<InkToolbarPenButton>>>,
86    pen_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
87    palette_subscription: RefCell<Option<(FerroList<Rc<dyn IBrush>>, u64)>>,
88    palette_panel: RefCell<Option<Ref<WrapPanel>>>,
89    slider: RefCell<Option<Ref<InkToolbarStrokeWidthSlider>>>,
90    syncing: Cell<bool>,
91    displayed_palette: RefCell<Vec<Rc<dyn IBrush>>>,
92    preview: RefCell<Option<(Ref<Canvas>, Ref<Path>)>>,
93    border_renderer: RefCell<BorderRenderHelper>,
94    entrance: RefCell<Option<Rc<CompositeDisposable>>>,
95    entrance_epoch: Cell<u64>,
96}
97ferro_class!(InkToolbarPenConfigurationControl:StackPanel);
98ferro_class_info!(InkToolbarPenConfigurationControl {
99    new: InkToolbarPenConfigurationControl::new
100});
101ferro_impl_classes!(InkToolbarPenConfigurationControl:StyledElementImpl,InteractiveImpl,ControlImpl,PanelImpl);
102impl LayoutableImpl for InkToolbarPenConfigurationControl {
103    fn measure_override(this: &Self, available_size: Size) -> Size {
104        let insets = this.layout_insets();
105        Self::parent_measure_override(this, available_size.deflate(insets)).inflate(insets)
106    }
107    fn arrange_override(this: &Self, final_size: Size) -> Size {
108        Self::parent_arrange_override(this, final_size.deflate(this.layout_insets()));
109        final_size
110    }
111}
112impl StackPanelImpl for InkToolbarPenConfigurationControl {
113    fn arrange_child(
114        this: &Self,
115        child: &Ref<Control>,
116        rect: Rect,
117        _panel_size: Size,
118        _orientation: Orientation,
119    ) {
120        let insets = this.layout_insets();
121        child.arrange(
122            rect.with_x(rect.x + insets.left)
123                .with_y(rect.y + insets.top),
124        );
125    }
126}
127impl VisualImpl for InkToolbarPenConfigurationControl {
128    fn on_attached_to_visual_tree(this: &Self, event: &VisualTreeAttachmentEventArgs) {
129        Self::parent_on_attached_to_visual_tree(this, event);
130        this.start_entrance();
131    }
132    fn on_detached_from_visual_tree(this: &Self, event: &VisualTreeAttachmentEventArgs) {
133        this.stop_entrance();
134        Self::parent_on_detached_from_visual_tree(this, event);
135    }
136    fn render(this: &Self, context: &mut DrawingContext) {
137        this.border_renderer.borrow_mut().render(
138            context,
139            this.bounds().size(),
140            round_panel_thickness(this, this.border_thickness()),
141            this.corner_radius(),
142            this.background_sizing(),
143            this.background().as_ref(),
144            this.border_brush().as_ref(),
145            &BoxShadows::default(),
146        );
147    }
148    fn clip_to_bounds_radius(this: &Self) -> Option<CornerRadius> {
149        Some(this.corner_radius())
150    }
151}
152impl InputElementImpl for InkToolbarPenConfigurationControl {
153    fn on_key_down(this: &Self, event: &KeyEventArgs) {
154        Self::parent_on_key_down(this, event);
155        if event.handled() {
156            return;
157        }
158        let panel = this.palette_panel.borrow().clone();
159        let Some(panel) = panel else {
160            return;
161        };
162        let children = panel.children().snapshot();
163        let Some(index) = children
164            .iter()
165            .position(|child| child.is_keyboard_focus_within())
166        else {
167            return;
168        };
169        let destination = match event.key {
170            Key::Left => index.checked_sub(1),
171            Key::Right => index.checked_add(1),
172            Key::Up => index.checked_sub(6),
173            Key::Down => index.checked_add(6),
174            Key::Home => Some(0),
175            Key::End => children.len().checked_sub(1),
176            _ => return,
177        };
178        if let Some(destination) = destination.filter(|&index| index < children.len()) {
179            let child = &children[destination];
180            if child.focus_with(NavigationMethod::Directional, event.key_modifiers) {
181                if let Some(pen) = this.pen_button() {
182                    pen.set_selected_brush_index(destination as i32);
183                }
184            }
185        }
186        event.set_handled(true);
187    }
188}
189impl FerroObjectImpl for InkToolbarPenConfigurationControl {
190    fn constructed(this: &Self) {
191        Self::parent_constructed(this);
192        this.set_orientation(Orientation::Vertical);
193        this.set_spacing(8.0);
194        let palette = WrapPanel::new();
195        palette.set_width(312.0);
196        palette.set_margin(Thickness::new(4.0, 0.0, 4.0, 2.0));
197        KeyboardNavigation::set_tab_navigation(&palette, KeyboardNavigationMode::Once);
198        strings::bind_name(&palette, "InkToolbarPenConfigurationColorsLabel");
199        let colors_title = TextBlock::new();
200        colors_title.set_font_weight(FontWeight::SemiBold);
201        colors_title.set_margin(Thickness::new(12.0, 0.0, 12.0, 0.0));
202        strings::bind_label(&colors_title, "InkToolbarPenConfigurationColorsLabel");
203        AutomationProperties::set_heading_level(&colors_title, 2);
204        let size_title = TextBlock::new();
205        size_title.set_font_weight(FontWeight::SemiBold);
206        size_title.set_margin(Thickness::new(12.0, 0.0, 12.0, 0.0));
207        strings::bind_label(&size_title, "InkToolbarPenConfigurationSizeLabel");
208        AutomationProperties::set_heading_level(&size_title, 2);
209        let preview_canvas = Canvas::new();
210        preview_canvas.set_width(296.0);
211        preview_canvas.set_margin(Thickness::new(12.0, 0.0, 12.0, 4.0));
212        preview_canvas.set_is_hit_test_visible(false);
213        let preview = Path::new();
214        preview.set_data(PolylineGeometry::with_points(PREVIEW_POINTS, false));
215        preview.set_stroke_line_cap(PenLineCap::Round);
216        preview.set_stroke_join(PenLineJoin::Round);
217        preview_canvas.children().add(preview.clone());
218        let slider = InkToolbarStrokeWidthSlider::new();
219        slider.set_width(296.0);
220        slider.set_height(44.0);
221        slider.set_margin(Thickness::new(12.0, 0.0, 12.0, 0.0));
222        let weak = this.to_ref().downgrade();
223        slider.property_changed(move |change| {
224            if change.property() != RangeBase::value_property().as_property() {
225                return;
226            }
227            if let Some(config) = weak.upgrade() {
228                if !config.syncing.get() {
229                    if let Some(pen) = config.pen_button() {
230                        pen.set_selected_stroke_width(change.get_new_value::<f64>());
231                    }
232                }
233            }
234        });
235        this.children().add(colors_title);
236        this.children().add(palette.clone());
237        this.children().add(size_title);
238        this.children().add(preview_canvas.clone());
239        this.children().add(slider.clone());
240        *this.palette_panel.borrow_mut() = Some(palette);
241        *this.slider.borrow_mut() = Some(slider);
242        *this.preview.borrow_mut() = Some((preview_canvas, preview));
243        let weak = this.to_ref().downgrade();
244        this.resources_changed(move |_| {
245            if let Some(config) = weak.upgrade() {
246                config.rebuild_palette();
247            }
248        });
249        this.bind_pen();
250    }
251    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
252        Self::parent_on_property_changed(this, change);
253        if change.property() == Self::pen_button_property().as_property() {
254            this.bind_pen();
255        }
256    }
257}
258ferro_properties! {impl InkToolbarPenConfigurationControl{
259    pub fn pen_button_property()->DirectProperty<InkToolbarPenConfigurationControl,Option<Ref<InkToolbarPenButton>>>{FerroProperty::register_direct::<InkToolbarPenConfigurationControl,_>("PenButton",InkToolbarPenConfigurationControl::pen_button,None,None)}
260    pub fn border_brush_property()->StyledProperty<Option<Rc<dyn IBrush>>>{Border::border_brush_property().add_owner::<InkToolbarPenConfigurationControl>()}
261    pub fn border_thickness_property()->StyledProperty<Thickness>{Border::border_thickness_property().add_owner::<InkToolbarPenConfigurationControl>()}
262    pub fn corner_radius_property()->StyledProperty<CornerRadius>{Border::corner_radius_property().add_owner::<InkToolbarPenConfigurationControl>()}
263    pub fn background_sizing_property()->StyledProperty<BackgroundSizing>{Border::background_sizing_property().add_owner::<InkToolbarPenConfigurationControl>()}
264    pub fn padding_property()->StyledProperty<Thickness>{Decorator::padding_property().add_owner::<InkToolbarPenConfigurationControl>()}
265}}
266impl InkToolbarPenConfigurationControl {
267    fn static_constructor() {
268        Visual::affects_render::<InkToolbarPenConfigurationControl>(&[
269            Self::border_brush_property().as_property(),
270            Self::border_thickness_property().as_property(),
271            Self::corner_radius_property().as_property(),
272            Self::background_sizing_property().as_property(),
273        ]);
274        Layoutable::affects_measure::<InkToolbarPenConfigurationControl>(&[
275            Self::padding_property().as_property(),
276            Self::border_thickness_property().as_property(),
277        ]);
278    }
279    pub fn construct() -> Self {
280        Self {
281            base: StackPanel::construct(),
282            pen: RefCell::new(None),
283            pen_subscription: RefCell::new(None),
284            palette_subscription: RefCell::new(None),
285            palette_panel: RefCell::new(None),
286            slider: RefCell::new(None),
287            syncing: Cell::new(false),
288            displayed_palette: RefCell::new(Vec::new()),
289            preview: RefCell::new(None),
290            border_renderer: RefCell::new(BorderRenderHelper::new()),
291            entrance: RefCell::new(None),
292            entrance_epoch: Cell::new(0),
293        }
294    }
295    pub fn new() -> Ref<Self> {
296        instantiate(Self::construct())
297    }
298    pub fn for_pen(pen: &InkToolbarPenButton) -> Ref<Self> {
299        let result = Self::new();
300        *result.pen.borrow_mut() = Some(pen.to_ref().downgrade());
301        result.raise_direct_property_changed(
302            Self::pen_button_property(),
303            &None,
304            &Some(pen.to_ref()),
305        );
306        result
307    }
308    pub fn pen_button(&self) -> Option<Ref<InkToolbarPenButton>> {
309        self.pen.borrow().as_ref().and_then(WeakRef::upgrade)
310    }
311    pub fn border_brush(&self) -> Option<Rc<dyn IBrush>> {
312        self.get_value(Self::border_brush_property())
313    }
314    pub fn set_border_brush(&self, value: Option<Rc<dyn IBrush>>) {
315        self.set_value(Self::border_brush_property(), value);
316    }
317    pub fn border_thickness(&self) -> Thickness {
318        self.get_value(Self::border_thickness_property())
319    }
320    pub fn set_border_thickness(&self, value: Thickness) {
321        self.set_value(Self::border_thickness_property(), value);
322    }
323    pub fn corner_radius(&self) -> CornerRadius {
324        self.get_value(Self::corner_radius_property())
325    }
326    pub fn set_corner_radius(&self, value: CornerRadius) {
327        self.set_value(Self::corner_radius_property(), value);
328    }
329    pub fn background_sizing(&self) -> BackgroundSizing {
330        self.get_value(Self::background_sizing_property())
331    }
332    pub fn set_background_sizing(&self, value: BackgroundSizing) {
333        self.set_value(Self::background_sizing_property(), value);
334    }
335    pub fn padding(&self) -> Thickness {
336        self.get_value(Self::padding_property())
337    }
338    pub fn set_padding(&self, value: Thickness) {
339        self.set_value(Self::padding_property(), value);
340    }
341    fn layout_insets(&self) -> Thickness {
342        round_panel_thickness(self, self.padding())
343            + round_panel_thickness(self, self.border_thickness())
344    }
345    pub fn stroke_width_slider(&self) -> Option<Ref<InkToolbarStrokeWidthSlider>> {
346        self.slider.borrow().clone()
347    }
348    /// Portable adaptation of InkToolbar.xaml's Grid.ChildrenTransitions / EntranceThemeTransition.
349    /// Each visible section enters from 40 DIPs to the right over 200 ms, staggered by 30 ms.
350    /// The framework clock, key-frame animation and property priorities own interpolation;
351    /// no local opacity or render-transform value is replaced by the transition.
352    fn start_entrance(&self) {
353        self.stop_entrance();
354        let clock = self.clock().or_else(|| {
355            FerroLocator::current()
356                .get_service::<dyn IGlobalClock>()
357                .map(|clock| clock as Rc<dyn IClock>)
358        });
359        let Some(clock) = clock else {
360            // A host without an animation clock still has a fully visible configuration.
361            return;
362        };
363        let children: Vec<_> = self
364            .children()
365            .snapshot()
366            .iter()
367            .filter(|child| child.is_visible())
368            .cloned()
369            .collect();
370        if children.is_empty() {
371            return;
372        }
373        let subscriptions = Rc::new(CompositeDisposable::new());
374        *self.entrance.borrow_mut() = Some(subscriptions.clone());
375        let epoch = self.entrance_epoch.get();
376        let remaining = Rc::new(Cell::new(children.len()));
377        for (index, child) in children.iter().enumerate() {
378            let transform = TransformGroup::new();
379            if let Some(original) = child.render_transform() {
380                transform
381                    .children()
382                    .add(MatrixTransform::with_matrix(original.value()).upcast());
383            }
384            transform.children().add(TranslateTransform::new().upcast());
385            let transform: Rc<dyn ITransform> = transform.into();
386            if let Some(overlay) = child.set_value_with_priority(
387                Visual::render_transform_property(),
388                Some(transform),
389                BindingPriority::Animation,
390            ) {
391                subscriptions.add(overlay);
392            }
393            let animation = Animation::new();
394            animation.set_duration(TimeSpan::from_milliseconds(200.0));
395            animation.set_delay(TimeSpan::from_milliseconds(index as f64 * 30.0));
396            animation.set_fill_mode(FillMode::Backward);
397            animation.set_easing(CubicEaseOut::new());
398            animation.children().add(KeyFrame::with_cue(
399                Cue::new(0.0),
400                [
401                    Setter::new(Visual::opacity_property(), 0.0) as _,
402                    Setter::new(TranslateTransform::x_property(), 40.0) as _,
403                ],
404            ));
405            // A valueless key-frame setter uses the current base value as its endpoint,
406            // including a new application opacity written while the entrance is running.
407            let final_opacity = Setter::empty();
408            final_opacity.set_property(Some(Visual::opacity_property().as_property()));
409            animation.children().add(KeyFrame::with_cue(
410                Cue::new(1.0),
411                [
412                    final_opacity as _,
413                    Setter::new(TranslateTransform::x_property(), 0.0) as _,
414                ],
415            ));
416            let weak = self.to_ref().downgrade();
417            let remaining = remaining.clone();
418            let completed = Rc::new(move || {
419                remaining.set(remaining.get().saturating_sub(1));
420                if remaining.get() == 0 {
421                    if let Some(configuration) = weak.upgrade() {
422                        if configuration.entrance_epoch.get() == epoch {
423                            configuration.stop_entrance();
424                        }
425                    }
426                }
427            });
428            subscriptions.add(animation.apply(
429                child,
430                Some(clock.clone()),
431                Observable::return_(true),
432                Some(completed),
433                true,
434            ));
435        }
436    }
437    fn stop_entrance(&self) {
438        self.entrance_epoch
439            .set(self.entrance_epoch.get().wrapping_add(1));
440        let subscriptions = self.entrance.borrow_mut().take();
441        if let Some(subscriptions) = subscriptions {
442            subscriptions.dispose();
443        }
444    }
445    fn bind_pen(&self) {
446        if let Some(subscription) = self.pen_subscription.borrow_mut().take() {
447            subscription.dispose();
448        }
449        if let Some(pen) = self.pen_button() {
450            let weak = self.to_ref().downgrade();
451            let subscription = pen.property_changed(move |change| {
452                if let Some(config) = weak.upgrade() {
453                    if !super::ink_toolbar_buttons::affects_pen_attributes(change.property()) {
454                        return;
455                    }
456                    if change.property() == InkToolbarPenButton::palette_property().as_property() {
457                        config.bind_palette();
458                    }
459                    let palette_changed = config.displayed_palette.borrow().as_slice()
460                        != pen_palette(&config).as_slice();
461                    if palette_changed {
462                        config.rebuild_palette();
463                    }
464                    config.synchronize();
465                }
466            });
467            *self.pen_subscription.borrow_mut() = Some(subscription);
468        }
469        self.bind_palette();
470        self.synchronize();
471    }
472    fn bind_palette(&self) {
473        if let Some((palette, token)) = self.palette_subscription.borrow_mut().take() {
474            palette.remove_collection_changed(token);
475        }
476        if let Some(pen) = self.pen_button() {
477            let palette = pen.palette();
478            let weak = self.to_ref().downgrade();
479            let token = palette.add_collection_changed(Rc::new(move |_| {
480                if let Some(config) = weak.upgrade() {
481                    config.rebuild_palette();
482                }
483            }));
484            *self.palette_subscription.borrow_mut() = Some((palette, token));
485        }
486        self.rebuild_palette();
487    }
488    fn rebuild_palette(&self) {
489        let panel = self.palette_panel.borrow().clone();
490        let Some(panel) = panel else {
491            return;
492        };
493        panel.children().clear();
494        let Some(pen) = self.pen_button() else {
495            return;
496        };
497        let effective_palette = pen.effective_palette();
498        *self.displayed_palette.borrow_mut() = effective_palette.clone();
499        for (index, brush) in effective_palette.iter().enumerate() {
500            let item = InkToolbarFlyoutItem::new();
501            item.set_kind(InkToolbarFlyoutItemKind::Radio);
502            item.set_width(52.0);
503            item.set_height(52.0);
504            item.classes().add("ink-palette-item");
505            let swatch = Ellipse::new();
506            swatch.set_width(32.0);
507            swatch.set_height(32.0);
508            swatch.set_fill(Some(brush.clone()));
509            if let Some((_, system)) = high_contrast_colors(self, false) {
510                swatch.set_stroke(Some(
511                    Rc::new(ImmutableSolidColorBrush::new(system)) as Rc<dyn IBrush>
512                ));
513                swatch.set_stroke_thickness(1.0);
514            }
515            item.set_content(Some(Rc::new(swatch.upcast::<Control>())));
516            bind_color_name(
517                &item,
518                brush.as_solid_color_brush().map(|brush| brush.color()),
519            );
520            let weak = pen.downgrade();
521            item.checked(move |_| {
522                if let Some(pen) = weak.upgrade() {
523                    pen.set_selected_brush_index(index as i32);
524                }
525            });
526            panel.children().add(item);
527        }
528        self.synchronize();
529    }
530    fn synchronize(&self) {
531        if self.syncing.replace(true) {
532            return;
533        }
534        let pen = self.pen_button();
535        let slider = self.slider.borrow().clone();
536        let panel = self.palette_panel.borrow().clone();
537        if let (Some(pen), Some(slider)) = (pen.as_ref(), slider) {
538            slider.set_minimum(pen.min_stroke_width());
539            slider.set_maximum(pen.max_stroke_width().max(pen.min_stroke_width()));
540            slider.set_range_value(pen.selected_stroke_width());
541        }
542        if let (Some(pen), Some(panel)) = (pen, panel) {
543            for (index, item) in panel.children().snapshot().iter().enumerate() {
544                if let Some(item) = item.cast::<InkToolbarFlyoutItem>() {
545                    let selected = index as i32 == pen.selected_brush_index();
546                    item.set_is_checked(Some(selected));
547                    if selected {
548                        KeyboardNavigation::set_tab_once_active_element(
549                            &panel,
550                            Some(item.upcast()),
551                        );
552                    }
553                }
554            }
555        }
556        self.syncing.set(false);
557        self.update_preview();
558    }
559    /// The sample stroke, rendered with the active pen's drawing attributes.
560    pub fn stroke_preview(&self) -> Option<Ref<Path>> {
561        self.preview.borrow().as_ref().map(|(_, path)| path.clone())
562    }
563    fn update_preview(&self) {
564        let pen = self.pen_button();
565        let preview = self.preview.borrow().clone();
566        let (Some(pen), Some((canvas, path))) = (pen, preview) else {
567            return;
568        };
569        let attrs = pen.create_ink_drawing_attributes().ok();
570        let brush = attrs
571            .map(|attrs| Rc::new(ImmutableSolidColorBrush::new(attrs.color)) as Rc<dyn IBrush>)
572            .or_else(|| pen.selected_brush());
573        path.set_stroke(brush);
574        path.set_stroke_thickness(
575            attrs
576                .map(|attrs| attrs.size.width)
577                .unwrap_or_else(|| pen.selected_stroke_width()),
578        );
579        path.set_opacity(if attrs.is_some_and(|attrs| attrs.draw_as_highlighter) {
580            0.5
581        } else {
582            1.0
583        });
584        let min_y = PREVIEW_POINTS
585            .iter()
586            .map(|point| point.y)
587            .fold(f64::INFINITY, f64::min);
588        let max_y = PREVIEW_POINTS
589            .iter()
590            .map(|point| point.y)
591            .fold(f64::NEG_INFINITY, f64::max);
592        let height = max_y - min_y + pen.max_stroke_width().max(path.stroke_thickness()) + 10.0;
593        canvas.set_height(height);
594        Canvas::set_top(&path, height / 2.0 - (min_y + max_y) / 2.0);
595    }
596}
597impl Drop for InkToolbarPenConfigurationControl {
598    fn drop(&mut self) {
599        self.stop_entrance();
600        if let Some(subscription) = self.pen_subscription.get_mut().take() {
601            subscription.dispose();
602        }
603        if let Some((palette, token)) = self.palette_subscription.get_mut().take() {
604            palette.remove_collection_changed(token);
605        }
606    }
607}
608fn pen_palette(config: &InkToolbarPenConfigurationControl) -> Vec<Rc<dyn IBrush>> {
609    config
610        .pen_button()
611        .map(|pen| pen.effective_palette())
612        .unwrap_or_default()
613}
614
615type ItemHandlers = Rc<HandlerList<dyn Fn(&InkToolbarFlyoutItem)>>;
616#[repr(C)]
617pub struct InkToolbarFlyoutItem {
618    base: ToggleButton,
619    checked: ItemHandlers,
620    unchecked: ItemHandlers,
621}
622ferro_class!(InkToolbarFlyoutItem:ToggleButton);
623ferro_class_info!(InkToolbarFlyoutItem {
624    new: InkToolbarFlyoutItem::new
625});
626ferro_impl_classes!(InkToolbarFlyoutItem: VisualImpl, LayoutableImpl, InteractiveImpl, InputElementImpl, TemplatedControlImpl, ContentControlImpl, ButtonImpl);
627impl ControlImpl for InkToolbarFlyoutItem {
628    fn on_create_automation_peer(this: &Self) -> Ref<ferroui_controls::automation::peers::AutomationPeer> {
629        super::InkToolbarFlyoutItemAutomationPeer::new(this).upcast()
630    }
631}
632
633impl StyledElementImpl for InkToolbarFlyoutItem {
634    fn style_key_override(this: &Self) -> &'static TypeInfo {
635        toolbar_style_key(this, ToggleButton::TYPE)
636    }
637}
638impl FerroObjectImpl for InkToolbarFlyoutItem {
639    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
640        Self::parent_on_property_changed(this, change);
641        if change.property() == ToggleButton::is_checked_property().as_property() {
642            if this.is_checked() == Some(true) {
643                this.uncheck_others_in_group();
644            }
645            let handlers = if this.is_checked() == Some(true) {
646                &this.checked
647            } else {
648                &this.unchecked
649            };
650            for (_, handler) in handlers.snapshot().iter() {
651                handler(this);
652            }
653        }
654    }
655}
656impl ToggleButtonImpl for InkToolbarFlyoutItem {
657    fn toggle(this: &Self) {
658        let value = match this.kind() {
659            InkToolbarFlyoutItemKind::Simple => return,
660            InkToolbarFlyoutItemKind::Radio => true,
661            InkToolbarFlyoutItemKind::Check | InkToolbarFlyoutItemKind::RadioCheck => {
662                this.is_checked() != Some(true)
663            }
664        };
665        this.set_current_value(ToggleButton::is_checked_property(), Some(value));
666    }
667}
668ferro_properties! {impl InkToolbarFlyoutItem{pub fn kind_property()->StyledProperty<InkToolbarFlyoutItemKind>{FerroProperty::register::<InkToolbarFlyoutItem,_>("Kind",InkToolbarFlyoutItemKind::Simple)}}}
669impl InkToolbarFlyoutItem {
670    pub fn construct() -> Self {
671        Self {
672            base: ToggleButton::construct(),
673            checked: Rc::new(HandlerList::new()),
674            unchecked: Rc::new(HandlerList::new()),
675        }
676    }
677    pub fn new() -> Ref<Self> {
678        instantiate(Self::construct())
679    }
680    /// As WinUI (`ForAllOthersInGroup`): a radio item that becomes checked unchecks the other
681    /// items of its kind in the same panel of the flyout.
682    fn uncheck_others_in_group(&self) {
683        let kind = self.kind();
684        if !matches!(
685            kind,
686            InkToolbarFlyoutItemKind::Radio | InkToolbarFlyoutItemKind::RadioCheck
687        ) {
688            return;
689        }
690        let Some(panel) = self.parent().and_then(|parent| parent.cast::<ferroui_controls::Panel>()) else {
691            return;
692        };
693        let this: Ref<Control> = self.to_ref().upcast();
694        for item in panel
695            .children()
696            .snapshot()
697            .iter()
698            .filter(|child| **child != this)
699            .filter_map(|child| child.cast::<InkToolbarFlyoutItem>())
700            .filter(|item| item.kind() == kind && item.is_checked() == Some(true))
701        {
702            item.set_is_checked(Some(false));
703        }
704    }
705    pub fn kind(&self) -> InkToolbarFlyoutItemKind {
706        self.get_value(Self::kind_property())
707    }
708    pub fn set_kind(&self, value: InkToolbarFlyoutItemKind) {
709        self.set_value(Self::kind_property(), value);
710    }
711    pub fn checked(
712        &self,
713        handler: impl Fn(&InkToolbarFlyoutItem) + 'static,
714    ) -> Rc<dyn IDisposable> {
715        subscribe(&self.checked, handler)
716    }
717    pub fn unchecked(
718        &self,
719        handler: impl Fn(&InkToolbarFlyoutItem) + 'static,
720    ) -> Rc<dyn IDisposable> {
721        subscribe(&self.unchecked, handler)
722    }
723}
724fn subscribe(
725    list: &ItemHandlers,
726    handler: impl Fn(&InkToolbarFlyoutItem) + 'static,
727) -> Rc<dyn IDisposable> {
728    let token = list.add(Rc::new(handler));
729    let weak = Rc::downgrade(list);
730    Disposable::create(move || {
731        if let Some(list) = weak.upgrade() {
732            list.remove(token);
733        }
734    })
735}
736
737// The source sample stroke; its fixed bounds avoid preview movement while resizing the nib.
738const PREVIEW_POINTS: [Point; 103] = [
739    Point::new(24.0, 28.5),
740    Point::new(26.175271, 26.452467),
741    Point::new(28.468156, 24.57762),
742    Point::new(30.860594, 22.862812),
743    Point::new(33.253032, 21.148004),
744    Point::new(35.745021, 19.593234),
745    Point::new(38.3185, 18.185855),
746    Point::new(40.891979, 16.778476),
747    Point::new(43.546946, 15.518487),
748    Point::new(46.26534, 14.39324),
749    Point::new(48.983733, 13.267993),
750    Point::new(51.765552, 12.277489),
751    Point::new(54.592735, 11.409078),
752    Point::new(57.419917, 10.540668),
753    Point::new(60.292463, 9.794352),
754    Point::new(63.192308, 9.1574822),
755    Point::new(66.092153, 8.5206123),
756    Point::new(69.019299, 7.9931887),
757    Point::new(71.955681, 7.5625631),
758    Point::new(74.892063, 7.1319376),
759    Point::new(77.837682, 6.7981102),
760    Point::new(80.774476, 6.5484328),
761    Point::new(83.711269, 6.2987555),
762    Point::new(86.639236, 6.1332282),
763    Point::new(89.540314, 6.0392029),
764    Point::new(92.12168, 5.9271775),
765    Point::new(94.691318, 5.9313799),
766    Point::new(97.248453, 6.040089),
767    Point::new(99.805587, 6.1487981),
768    Point::new(102.35022, 6.3620141),
769    Point::new(104.88157, 6.668016),
770    Point::new(107.41293, 6.974018),
771    Point::new(109.931, 7.3728059),
772    Point::new(112.43502, 7.852659),
773    Point::new(114.93904, 8.332512),
774    Point::new(117.42901, 8.8934302),
775    Point::new(119.90414, 9.5236926),
776    Point::new(122.37928, 10.153955),
777    Point::new(124.83959, 10.853562),
778    Point::new(127.28429, 11.610792),
779    Point::new(129.72899, 12.368022),
780    Point::new(132.15809, 13.182876),
781    Point::new(134.57081, 14.043632),
782    Point::new(136.98352, 14.904388),
783    Point::new(139.37986, 15.811047),
784    Point::new(141.75904, 16.751888),
785    Point::new(144.13822, 17.692728),
786    Point::new(146.50025, 18.667751),
787    Point::new(148.84434, 19.665234),
788    Point::new(150.92055, 20.558561),
789    Point::new(153.02009, 21.393996),
790    Point::new(155.13997, 22.171709),
791    Point::new(157.25986, 22.949423),
792    Point::new(159.40009, 23.669414),
793    Point::new(161.55771, 24.331854),
794    Point::new(163.71533, 24.994294),
795    Point::new(165.89033, 25.599182),
796    Point::new(168.07973, 26.146689),
797    Point::new(170.26914, 26.694195),
798    Point::new(172.47295, 27.184321),
799    Point::new(174.6882, 27.617235),
800    Point::new(176.90346, 28.050149),
801    Point::new(179.13015, 28.425851),
802    Point::new(181.3653, 28.744513),
803    Point::new(183.60046, 29.063175),
804    Point::new(185.84408, 29.324795),
805    Point::new(188.0932, 29.529545),
806    Point::new(190.34231, 29.734295),
807    Point::new(192.59692, 29.882174),
808    Point::new(194.85406, 29.973352),
809    Point::new(197.11119, 30.06453),
810    Point::new(199.37085, 30.099008),
811    Point::new(201.63006, 30.076955),
812    Point::new(203.88927, 30.054901),
813    Point::new(206.14803, 29.976318),
814    Point::new(208.40338, 29.841374),
815    Point::new(210.65872, 29.70643),
816    Point::new(212.91064, 29.515126),
817    Point::new(215.15617, 29.267631),
818    Point::new(219.64724, 28.772643),
819    Point::new(224.11274, 28.052893),
820    Point::new(228.52891, 27.109745),
821    Point::new(230.73699, 26.638171),
822    Point::new(232.93274, 26.110747),
823    Point::new(235.11319, 25.527643),
824    Point::new(237.29363, 24.94454),
825    Point::new(239.45877, 24.305756),
826    Point::new(241.60564, 23.611464),
827    Point::new(243.7525, 22.917171),
828    Point::new(245.88109, 22.167369),
829    Point::new(247.98843, 21.362228),
830    Point::new(250.09577, 20.557087),
831    Point::new(252.18187, 19.696606),
832    Point::new(254.24374, 18.780957),
833    Point::new(257.44071, 17.278777),
834    Point::new(260.5682, 15.598548),
835    Point::new(263.55223, 13.698859),
836    Point::new(265.04425, 12.749015),
837    Point::new(266.50041, 11.744305),
838    Point::new(267.91145, 10.679554),
839    Point::new(269.32249, 9.6148034),
840    Point::new(270.68842, 8.4900108),
841    Point::new(272.0, 7.3),
842];
843
844#[cfg(test)]
845#[path = "ink_toolbar_configuration_tests.rs"]
846mod tests;