Skip to main content

ferroui_controls/
selectable_text_block.rs

1use crate::documents::InlineCollection;
2use crate::text_block::InlinesTextSource;
3use crate::utils::{ClipboardHelper, PrimarySelectionHelper, StringUtils};
4use crate::{Application, ControlImpl, TextBlock, TextBlockImpl, TextBlockImplExt, TextBox, TopLevel};
5use ferroui_base::input::platform::ClipboardExtensions;
6use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
7use std::any::Any;
8use ferroui_base::input::{
9    FocusChangedEventArgs, InputElement, InputElementImpl, InputElementImplExt, KeyEventArgs, KeyGesture,
10    KeyModifiers, MouseButton, PointerEventArgs, PointerPressedEventArgs, PointerReleasedEventArgs,
11};
12use ferroui_base::interactivity::{
13    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
14};
15use ferroui_base::layout::LayoutableImpl;
16use ferroui_base::media::text_formatting::{
17    FormattedTextSource, GenericTextParagraphProperties, GenericTextRunProperties, ITextSource, TextLayout,
18    TextRunProperties,
19};
20use ferroui_base::media::{BaselineAlignment, DrawingContext, IBrush, Typeface};
21use ferroui_base::reactive::IDisposable;
22use ferroui_base::utilities::{ReadOnlyMemory, ValueSpan};
23use ferroui_base::{
24    ferro_class, ferro_property, ferro_routed_event, instantiate, DirectProperty, FerroObjectImpl,
25    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Matrix, PixelRect, Point, Ref, StyledElementImpl,
26    StyledProperty, StyledPropertyMetadata, Visual, VisualImpl,
27};
28use std::cell::{Cell, RefCell};
29use std::rc::Rc;
30
31/// A control that displays a block of formatted text whose content can be
32/// selected and copied.
33#[repr(C)]
34pub struct SelectableTextBlock {
35    base: TextBlock,
36    can_copy: Cell<bool>,
37    word_selection_start: Cell<i32>,
38    selection_at_pointer_press: Cell<(i32, i32)>,
39    inlines_invalidated: RefCell<Option<Rc<dyn IDisposable>>>,
40    /// The UTF-16 form of the text the last layout was created for, so that
41    /// a new layout of unchanged text (a new selection, a new constraint)
42    /// does not transcode it again.
43    utf16_text: RefCell<Option<(String, ReadOnlyMemory<u16>)>>,
44}
45
46ferro_class!(SelectableTextBlock: TextBlock);
47// The `markup:` part is declared here and not generated: the public methods of the class are
48// declared for markup, the command of the context menu of the text block in the control themes
49// (`Command="{Binding $parent[SelectableTextBlock].Copy}"`).
50ferroui_base::ferro_class_info!(SelectableTextBlock {
51    new: SelectableTextBlock::new,
52    markup: {
53        methods: [
54            fn Copy() => SelectableTextBlock::copy,
55            fn SelectAll() => SelectableTextBlock::select_all,
56            fn ClearSelection() => SelectableTextBlock::clear_selection,
57        ],
58        fields: [
59            CopyingToClipboardEvent: ferroui_base::interactivity::RoutedEvent<ferroui_base::interactivity::RoutedEventArgs> => || *SelectableTextBlock::copying_to_clipboard_event(),
60        ],
61    },
62});
63
64ferroui_base::ferro_impl_classes!(SelectableTextBlock: StyledElementImpl);
65ferroui_base::ferro_impl_classes!(SelectableTextBlock: VisualImpl);
66ferroui_base::ferro_impl_classes!(SelectableTextBlock: LayoutableImpl);
67ferroui_base::ferro_impl_classes!(SelectableTextBlock: InteractiveImpl);
68ferroui_base::ferro_impl_classes!(SelectableTextBlock: ControlImpl);
69
70impl FerroObjectImpl for SelectableTextBlock {
71    fn constructed(this: &Self) {
72        // The static constructor runs before the instance constructors: the
73        // base constructor already raises property changes handled here.
74        Self::parent_constructed(this);
75    }
76
77    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
78        Self::parent_on_property_changed(this, change);
79
80        if change.property() == TextBlock::inlines_property().as_property() {
81            let (old_inlines, new_inlines) = change.get_old_and_new_value::<Option<InlineCollection>>();
82
83            if old_inlines.is_some() {
84                let subscription = this.inlines_invalidated.borrow_mut().take();
85                if let Some(subscription) = subscription {
86                    subscription.dispose();
87                }
88            }
89
90            if let Some(new_inlines) = new_inlines {
91                let weak = this.to_ref().downgrade();
92                let subscription = new_inlines.invalidated(move || {
93                    if let Some(this) = weak.upgrade() {
94                        this.on_text_or_inlines_changed();
95                    }
96                });
97                *this.inlines_invalidated.borrow_mut() = Some(subscription);
98            }
99
100            this.on_text_or_inlines_changed();
101        } else if change.property() == TextBlock::text_property().as_property() {
102            this.on_text_or_inlines_changed();
103        } else if change.property() == Self::selection_start_property().as_property()
104            || change.property() == Self::selection_end_property().as_property()
105        {
106            this.raise_direct_property_changed(Self::selected_text_property(), &String::new(), &String::new());
107            this.update_command_states();
108            this.invalidate_text_layout();
109        } else if change.property() == Self::selection_foreground_brush_property().as_property() {
110            this.invalidate_text_layout();
111        }
112    }
113}
114
115impl InputElementImpl for SelectableTextBlock {
116    fn on_got_focus(this: &Self, e: &FocusChangedEventArgs) {
117        Self::parent_on_got_focus(this, e);
118
119        this.update_command_states();
120    }
121
122    fn on_lost_focus(this: &Self, e: &FocusChangedEventArgs) {
123        Self::parent_on_lost_focus(this, e);
124
125        if !this.context_flyout().is_some_and(|flyout| flyout.is_open())
126            && !this.context_menu().is_some_and(|menu| menu.is_open())
127        {
128            this.clear_selection();
129        }
130
131        this.update_command_states();
132    }
133
134    fn on_key_down(this: &Self, e: &KeyEventArgs) {
135        Self::parent_on_key_down(this, e);
136
137        let mut handled = false;
138        let keymap = Application::current()
139            .expect("the application")
140            .platform_settings()
141            .expect("the platform settings of the application")
142            .hotkey_configuration();
143
144        let matches = |gestures: &[KeyGesture]| gestures.iter().any(|g| g.matches(Some(e)));
145
146        if matches(&keymap.copy) {
147            this.copy();
148            handled = true;
149        } else if matches(&keymap.select_all) {
150            this.select_all();
151            handled = true;
152        }
153
154        e.set_handled(handled);
155    }
156
157    fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
158        Self::parent_on_pointer_pressed(this, e);
159
160        this.selection_at_pointer_press.set(this.get_selection_range());
161
162        let text = this.content_text();
163        let click_info = e.get_current_point(Some(this));
164
165        if let Some(text) = text.filter(|_| click_info.properties.is_left_button_pressed) {
166            let text: Vec<u16> = text.encode_utf16().collect();
167            let padding = this.padding();
168
169            // A FerroUI addition: the text is lower by the height layout rounding adds above it
170            // (`TextBlock.LayoutRoundingPlacement`).
171            let point = e.get_position(Some(this))
172                - Point::new(padding.left, padding.top + this.layout_rounding_offset());
173
174            let click_to_select = e.key_modifiers().contains(KeyModifiers::SHIFT);
175
176            let old_index = this.selection_start();
177
178            let hit = this.text_layout().hit_test_point(point);
179            let index = hit.text_position();
180
181            match e.click_count() {
182                1 => {
183                    if click_to_select {
184                        let word_selection_start = this.word_selection_start.get();
185
186                        if word_selection_start >= 0 {
187                            let previous_word = StringUtils::previous_word(&text, index);
188
189                            if index > word_selection_start {
190                                this.set_current_value(Self::selection_end_property(), StringUtils::next_word(&text, index));
191                            }
192
193                            if index < word_selection_start || previous_word == word_selection_start {
194                                this.set_current_value(Self::selection_start_property(), previous_word);
195                            }
196                        } else {
197                            this.set_current_value(Self::selection_start_property(), old_index.min(index));
198                            this.set_current_value(Self::selection_end_property(), old_index.max(index));
199                        }
200                    } else {
201                        this.set_current_value(Self::selection_start_property(), index);
202                        this.set_current_value(Self::selection_end_property(), index);
203                        this.word_selection_start.set(-1);
204                    }
205                }
206                2 => {
207                    if !StringUtils::is_start_of_word(&text, index) {
208                        this.set_current_value(Self::selection_start_property(), StringUtils::previous_word(&text, index));
209                    }
210
211                    this.word_selection_start.set(this.selection_start());
212
213                    if !StringUtils::is_end_of_word(&text, index) {
214                        this.set_current_value(Self::selection_end_property(), StringUtils::next_word(&text, index));
215                    }
216                }
217                3 => {
218                    this.word_selection_start.set(-1);
219
220                    this.select_all();
221                }
222                _ => {}
223            }
224        }
225
226        let element: Ref<InputElement> = this.to_ref().upcast();
227        e.pointer().capture(Some(&element));
228        e.set_handled(true);
229    }
230
231    fn on_pointer_moved(this: &Self, e: &PointerEventArgs) {
232        Self::parent_on_pointer_moved(this, e);
233
234        // The selection should not change during a pointer move if the user right clicks.
235        if this.is_captured_by(e) && e.get_current_point(Some(this)).properties.is_left_button_pressed {
236            let text = this.content_text();
237            let padding = this.padding();
238
239            // A FerroUI addition: the text is lower by the height layout rounding adds above it
240            // (`TextBlock.LayoutRoundingPlacement`).
241            let point = e.get_position(Some(this))
242                - Point::new(padding.left, padding.top + this.layout_rounding_offset());
243
244            let hit = this.text_layout().hit_test_point(point);
245            let text_position = hit.text_position();
246
247            let word_selection_start = this.word_selection_start.get();
248
249            match text {
250                Some(text) if word_selection_start >= 0 => {
251                    let text: Vec<u16> = text.encode_utf16().collect();
252                    let distance = text_position - word_selection_start;
253
254                    if distance <= 0 {
255                        this.set_current_value(Self::selection_start_property(), StringUtils::previous_word(&text, text_position));
256                    }
257
258                    if distance >= 0 {
259                        if this.selection_start() != word_selection_start {
260                            this.set_current_value(Self::selection_start_property(), word_selection_start);
261                        }
262
263                        this.set_current_value(Self::selection_end_property(), StringUtils::next_word(&text, text_position));
264                    }
265                }
266                _ => {
267                    this.set_current_value(Self::selection_end_property(), text_position);
268                }
269            }
270        }
271    }
272
273    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
274        Self::parent_on_pointer_released(this, e);
275
276        if !this.is_captured_by(e) {
277            return;
278        }
279
280        if e.initial_press_mouse_button() == MouseButton::Right {
281            let padding = this.padding();
282
283            // A FerroUI addition: the text is lower by the height layout rounding adds above it
284            // (`TextBlock.LayoutRoundingPlacement`).
285            let point = e.get_position(Some(this))
286                - Point::new(padding.left, padding.top + this.layout_rounding_offset());
287
288            let hit = this.text_layout().hit_test_point(point);
289
290            // A point below the text hits one past its end, so clamp it as the text box does for its caret.
291            let caret_index = TextBox::coerce_caret_index(this, hit.text_position());
292
293            // See if the mouse clicked inside the current selection:
294            // if it did not, the selection moves to where the user clicked.
295            let first_selection = this.selection_start().min(this.selection_end());
296            let last_selection = this.selection_start().max(this.selection_end());
297            let did_click_in_selection = this.selection_start() != this.selection_end()
298                && caret_index >= first_selection
299                && caret_index <= last_selection;
300            if !did_click_in_selection {
301                this.set_current_value(Self::selection_start_property(), caret_index);
302                this.set_current_value(Self::selection_end_property(), caret_index);
303            }
304        }
305
306        let selection = this.get_selection_range();
307        if e.initial_press_mouse_button() == MouseButton::Left
308            && selection.0 != selection.1
309            && selection != this.selection_at_pointer_press.get()
310        {
311            // The pointer gesture changed the selection, publish it to the primary selection.
312            let target = this.to_ref();
313            drop(PrimarySelectionHelper::publish_text_async(this, move || Some(target.get_selection())));
314        }
315
316        e.pointer().capture(None);
317    }
318}
319
320impl TextBlockImpl for SelectableTextBlock {
321    fn create_text_layout(this: &Self, text: Option<&str>) -> Rc<TextLayout> {
322        let typeface =
323            Typeface::with_style(this.font_family(), this.font_style(), this.font_weight(), this.font_stretch());
324
325        let default_properties: Rc<dyn TextRunProperties> = Rc::new(GenericTextRunProperties::with_all(
326            typeface.clone(),
327            this.font_size(),
328            this.text_decorations(),
329            this.foreground(),
330            None,
331            BaselineAlignment::Baseline,
332            None,
333            this.font_features(),
334        ));
335
336        let paragraph_properties = GenericTextParagraphProperties::with_all(
337            this.flow_direction(),
338            this.text_alignment(),
339            true,
340            false,
341            default_properties.clone(),
342            this.text_wrapping(),
343            this.line_height(),
344            0.0,
345            this.letter_spacing(),
346        );
347        paragraph_properties.set_line_spacing(this.line_spacing());
348
349        let mut text_style_overrides: Option<Vec<ValueSpan<Rc<dyn TextRunProperties>>>> = None;
350        let selection_start = this.selection_start();
351        let selection_end = this.selection_end();
352        let start = selection_start.min(selection_end);
353        let length = selection_start.max(selection_end) - start;
354
355        if length > 0 {
356            if let Some(selection_foreground_brush) = this.selection_foreground_brush() {
357                let selection_properties = |typeface: Typeface, font_features| -> Rc<dyn TextRunProperties> {
358                    Rc::new(GenericTextRunProperties::with_all(
359                        typeface,
360                        this.font_size(),
361                        None,
362                        Some(selection_foreground_brush.clone()),
363                        None,
364                        BaselineAlignment::Baseline,
365                        None,
366                        font_features,
367                    ))
368                };
369
370                if let Some(text_runs) = this.text_runs() {
371                    // Apply the selection foreground without changing the original text formatting:
372                    // each run keeps its own typeface and font features and only the foreground
373                    // brush is overridden.
374                    let mut accumulated_length = 0;
375
376                    for text_run in text_runs.iter() {
377                        let run_length = text_run.text().len() as i32;
378
379                        if accumulated_length + run_length <= start || accumulated_length >= start + length {
380                            accumulated_length += run_length;
381                            continue;
382                        }
383
384                        let overlap_start = start.max(accumulated_length);
385                        let overlap_end = (start + length).min(accumulated_length + run_length);
386                        let overlap_length = overlap_end - overlap_start;
387
388                        let run_properties = text_run.properties();
389
390                        text_style_overrides.get_or_insert_with(Vec::new).push(ValueSpan::new(
391                            overlap_start,
392                            overlap_length,
393                            selection_properties(
394                                run_properties.map_or_else(|| typeface.clone(), |p| p.typeface().clone()),
395                                match run_properties.and_then(|p| p.font_features()) {
396                                    Some(font_features) => Some(font_features.clone()),
397                                    None => this.font_features(),
398                                },
399                            ),
400                        ));
401
402                        accumulated_length += run_length;
403                    }
404                } else {
405                    text_style_overrides = Some(vec![ValueSpan::new(
406                        start,
407                        length,
408                        selection_properties(typeface.clone(), this.font_features()),
409                    )]);
410                }
411            }
412        }
413
414        let text_style_overrides: Option<Rc<[ValueSpan<Rc<dyn TextRunProperties>>]>> =
415            text_style_overrides.map(Rc::from);
416
417        let text_source: Rc<dyn ITextSource> = if this.has_complex_content() {
418            this.ensure_text_runs();
419
420            Rc::new(InlinesTextSource::new(this.text_runs().unwrap_or_default(), text_style_overrides))
421        } else {
422            Rc::new(FormattedTextSource::new(
423                this.utf16_text(text.unwrap_or("")),
424                default_properties,
425                text_style_overrides,
426            ))
427        };
428
429        let max_size = this.get_max_size_from_constraint();
430
431        Rc::new(TextLayout::from_text_source(
432            text_source,
433            Rc::new(paragraph_properties),
434            Some(this.text_trimming()),
435            max_size.width,
436            max_size.height,
437            this.max_lines(),
438            None,
439        ))
440    }
441
442    fn render_text_layout(this: &Self, context: &mut DrawingContext, origin: Point) {
443        let selection_start = this.selection_start();
444        let selection_end = this.selection_end();
445
446        if selection_start != selection_end {
447            if let Some(selection_brush) = this.selection_brush() {
448                let start = selection_start.min(selection_end);
449                let length = selection_start.max(selection_end) - start;
450
451                let rects = this.text_layout().hit_test_text_range(start, length);
452
453                let state = context.push_transform(Matrix::create_translation(origin.x, origin.y));
454
455                for rect in rects {
456                    context.fill_rectangle(&selection_brush, PixelRect::from_rect(rect, 1.0).to_rect(1.0), 0.0);
457                }
458
459                context.pop(state);
460            }
461        }
462
463        Self::parent_render_text_layout(this, context, origin);
464    }
465}
466
467ferroui_base::ferro_properties! { impl SelectableTextBlock {
468    ferro_property!(
469        /// Defines the `SelectionStart` property.
470        pub fn selection_start_property() -> StyledProperty<i32> {
471            TextBox::selection_start_property()
472                .add_owner_with::<SelectableTextBlock>(StyledPropertyMetadata::new(None).with_coerce(TextBox::coerce_caret_index))
473        }
474    );
475
476    ferro_property!(
477        /// Defines the `SelectionEnd` property.
478        pub fn selection_end_property() -> StyledProperty<i32> {
479            TextBox::selection_end_property()
480                .add_owner_with::<SelectableTextBlock>(StyledPropertyMetadata::new(None).with_coerce(TextBox::coerce_caret_index))
481        }
482    );
483
484    ferro_property!(
485        /// Defines the `SelectedText` property.
486        pub fn selected_text_property() -> DirectProperty<SelectableTextBlock, String> {
487            FerroProperty::register_direct::<SelectableTextBlock, _>(
488                "SelectedText",
489                |o| o.selected_text(),
490                None,
491                String::new(),
492            )
493        }
494    );
495
496    ferro_property!(
497        /// Defines the `SelectionBrush` property.
498        pub fn selection_brush_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
499            TextBox::selection_brush_property().add_owner::<SelectableTextBlock>()
500        }
501    );
502
503    ferro_property!(
504        /// Defines the `SelectionForegroundBrush` property.
505        pub fn selection_foreground_brush_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
506            TextBox::selection_foreground_brush_property().add_owner::<SelectableTextBlock>()
507        }
508    );
509
510    ferro_property!(
511        /// Defines the `CanCopy` property.
512        pub fn can_copy_property() -> DirectProperty<SelectableTextBlock, bool> {
513            TextBox::can_copy_property().add_owner::<SelectableTextBlock>(|o| o.can_copy(), None, None)
514        }
515    );
516} }
517
518impl SelectableTextBlock {
519    ferro_routed_event!(
520        /// Defines the `CopyingToClipboard` event.
521        pub fn copying_to_clipboard_event() -> RoutedEvent<RoutedEventArgs> {
522            RoutedEvent::register::<SelectableTextBlock, _>("CopyingToClipboard", RoutingStrategies::BUBBLE)
523        }
524    );
525
526    fn static_constructor() {
527        InputElement::focusable_property().override_default_value::<SelectableTextBlock>(true);
528        Visual::affects_render::<SelectableTextBlock>(&[
529            Self::selection_start_property().as_property(),
530            Self::selection_end_property().as_property(),
531            Self::selection_brush_property().as_property(),
532        ]);
533    }
534
535    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
536    pub fn construct() -> Self {
537        Self {
538            base: TextBlock::construct(),
539            can_copy: Cell::new(false),
540            word_selection_start: Cell::new(-1),
541            selection_at_pointer_press: Cell::new((0, 0)),
542            inlines_invalidated: RefCell::new(None),
543            utf16_text: RefCell::new(None),
544        }
545    }
546
547    /// Initializes a new instance of the `SelectableTextBlock` class.
548    pub fn new() -> Ref<Self> {
549        instantiate(Self::construct())
550    }
551
552    /// Raised when the selection is about to be copied to the clipboard;
553    /// marking the event handled cancels the copy.
554    pub fn copying_to_clipboard(
555        &self,
556        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
557    ) -> RoutedEventHandlerToken {
558        self.add_handler(Self::copying_to_clipboard_event(), handler)
559    }
560
561    /// The brush that highlights selected text.
562    pub fn selection_brush(&self) -> Option<Rc<dyn IBrush>> {
563        self.get_value(Self::selection_brush_property())
564    }
565
566    pub fn set_selection_brush(&self, value: Option<Rc<dyn IBrush>>) {
567        self.set_value(Self::selection_brush_property(), value)
568    }
569
570    /// The brush that is used for the foreground of selected text.
571    pub fn selection_foreground_brush(&self) -> Option<Rc<dyn IBrush>> {
572        self.get_value(Self::selection_foreground_brush_property())
573    }
574
575    pub fn set_selection_foreground_brush(&self, value: Option<Rc<dyn IBrush>>) {
576        self.set_value(Self::selection_foreground_brush_property(), value)
577    }
578
579    /// The character index of the beginning of the current selection.
580    pub fn selection_start(&self) -> i32 {
581        self.get_value(Self::selection_start_property())
582    }
583
584    pub fn set_selection_start(&self, value: i32) {
585        self.set_value(Self::selection_start_property(), value)
586    }
587
588    /// The character index of the end of the current selection.
589    pub fn selection_end(&self) -> i32 {
590        self.get_value(Self::selection_end_property())
591    }
592
593    pub fn set_selection_end(&self, value: i32) {
594        self.set_value(Self::selection_end_property(), value)
595    }
596
597    /// The content of the current selection.
598    pub fn selected_text(&self) -> String {
599        self.get_selection()
600    }
601
602    /// Whether the copy command can be executed.
603    pub fn can_copy(&self) -> bool {
604        self.can_copy.get()
605    }
606
607    fn set_can_copy(&self, value: bool) {
608        self.set_and_raise_cell(Self::can_copy_property(), &self.can_copy, value);
609    }
610
611    /// Copies the current selection to the clipboard.
612    ///
613    /// Like the asynchronous method of the reference, everything up to the
614    /// clipboard call runs before this returns. A failure platform
615    /// clipboards are known for (see
616    /// `ClipboardHelper::is_expected_clipboard_exception`) is logged; any
617    /// other failure, and a panic of the clipboard, is raised on the
618    /// dispatcher.
619    pub fn copy(&self) {
620        if !self.can_copy.get() {
621            return;
622        }
623
624        let text = self.get_selection();
625
626        if text.is_empty() {
627            return;
628        }
629
630        let event_args = RoutedEventArgs::with_event(Self::copying_to_clipboard_event());
631
632        self.raise_event(&event_args);
633
634        if !event_args.handled() {
635            let Some(clipboard) = TopLevel::get_top_level(Some(self)).and_then(|top_level| top_level.clipboard())
636            else {
637                return;
638            };
639
640            let this = self.to_ref();
641
642            ClipboardHelper::start(async move {
643                match clipboard.set_text_async(Some(&text)).await {
644                    Ok(()) => {}
645                    Err(ex) if ClipboardHelper::is_expected_clipboard_exception(&ex) => {
646                        if let Some(logger) = Logger::try_get(LogEventLevel::Warning, LogArea::CONTROL) {
647                            let source: &dyn Any = &this;
648                            logger.log_with_values(Some(source), "Failed to write text to clipboard: {Error}", &[&ex]);
649                        }
650                    }
651                    Err(ex) => return Err(ex),
652                }
653
654                Ok(())
655            });
656        }
657    }
658
659    /// Selects all text of the text block.
660    pub fn select_all(&self) {
661        let text = self.content_text();
662
663        self.set_current_value(Self::selection_start_property(), 0);
664        self.set_current_value(Self::selection_end_property(), text.as_deref().map_or(0, utf16_length));
665    }
666
667    /// Clears the current selection.
668    pub fn clear_selection(&self) {
669        self.set_current_value(Self::selection_end_property(), self.selection_start());
670    }
671
672    /// The text the selection indices refer to: the text of the inlines when
673    /// there are any, the `Text` otherwise.
674    fn content_text(&self) -> Option<String> {
675        if self.has_complex_content() {
676            self.inlines().and_then(|inlines| inlines.text())
677        } else {
678            self.text()
679        }
680    }
681
682    /// Whether the pointer of a pointer event is captured by this control.
683    fn is_captured_by(&self, e: &PointerEventArgs) -> bool {
684        e.pointer().captured().is_some_and(|captured| captured == self.to_ref())
685    }
686
687    fn get_selection_range(&self) -> (i32, i32) {
688        let selection_start = self.selection_start();
689        let selection_end = self.selection_end();
690
691        (selection_start.min(selection_end), selection_start.max(selection_end))
692    }
693
694    fn on_text_or_inlines_changed(&self) {
695        self.coerce_value(Self::selection_start_property().as_property());
696        self.coerce_value(Self::selection_end_property().as_property());
697        self.raise_direct_property_changed(Self::selected_text_property(), &String::new(), &String::new());
698        self.update_command_states();
699    }
700
701    fn update_command_states(&self) {
702        self.set_can_copy(self.has_selection());
703    }
704
705    /// Reports the same emptiness conditions as `get_selection`, without
706    /// building the selected string.
707    fn has_selection(&self) -> bool {
708        let selection_start = self.selection_start();
709        let selection_end = self.selection_end();
710        let start = selection_start.min(selection_end);
711        let end = selection_start.max(selection_end);
712
713        if start == end {
714            return false;
715        }
716
717        let text_length = self.content_text().as_deref().map_or(0, utf16_length);
718
719        text_length > 0 && end <= text_length
720    }
721
722    fn get_selection(&self) -> String {
723        let Some(text) = self.content_text() else {
724            return String::new();
725        };
726
727        let text_length = utf16_length(&text);
728
729        if text_length == 0 {
730            return String::new();
731        }
732
733        let selection_start = self.selection_start();
734        let selection_end = self.selection_end();
735        let start = selection_start.min(selection_end);
736        let end = selection_start.max(selection_end);
737
738        if start == end || text_length < end {
739            return String::new();
740        }
741
742        let length = (end - start).max(0);
743
744        // The indices are UTF-16 code units; a selection that splits a
745        // surrogate pair yields a replacement character for the lone half.
746        let selected_text: Vec<u16> = text.encode_utf16().skip(start.max(0) as usize).take(length as usize).collect();
747
748        String::from_utf16_lossy(&selected_text)
749    }
750
751    /// The UTF-16 form of `text`, transcoded only when it differs from the
752    /// text of the previous layout.
753    fn utf16_text(&self, text: &str) -> ReadOnlyMemory<u16> {
754        if let Some((cached_text, utf16)) = self.utf16_text.borrow().as_ref() {
755            if cached_text == text {
756                return utf16.clone();
757            }
758        }
759
760        let utf16 = ReadOnlyMemory::<u16>::from_str(text);
761        *self.utf16_text.borrow_mut() = Some((text.to_owned(), utf16.clone()));
762        utf16
763    }
764}
765
766/// The length of `text` in UTF-16 code units.
767fn utf16_length(text: &str) -> i32 {
768    text.encode_utf16().count() as i32
769}