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#[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 utf16_text: RefCell<Option<(String, ReadOnlyMemory<u16>)>>,
44}
45
46ferro_class!(SelectableTextBlock: TextBlock);
47ferroui_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 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 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 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 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 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 let caret_index = TextBox::coerce_caret_index(this, hit.text_position());
292
293 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 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 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 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 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 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 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 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 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 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 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 pub fn new() -> Ref<Self> {
549 instantiate(Self::construct())
550 }
551
552 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 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 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 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 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 pub fn selected_text(&self) -> String {
599 self.get_selection()
600 }
601
602 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 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 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 pub fn clear_selection(&self) {
669 self.set_current_value(Self::selection_end_property(), self.selection_start());
670 }
671
672 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 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 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 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 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
766fn utf16_length(text: &str) -> i32 {
768 text.encode_utf16().count() as i32
769}