1use std::sync::Arc;
2
3use emath::TSTransform;
4
5use crate::{
6 Context, CursorIcon, Event, Galley, Id, LayerId, Plugin, Pos2, Rect, Response, Ui,
7 ViewportIdMap, layers::ShapeIdx, text::CCursor, text_selection::CCursorRange,
8};
9
10use super::{
11 TextCursorState,
12 text_cursor_state::cursor_rect,
13 visuals::{RowVertexIndices, paint_text_selection},
14};
15
16const DEBUG: bool = false; #[derive(Clone, Copy)]
21struct WidgetTextCursor {
22 widget_id: Id,
23 ccursor: CCursor,
24
25 pos: Pos2,
27}
28
29impl WidgetTextCursor {
30 fn new(
31 widget_id: Id,
32 cursor: impl Into<CCursor>,
33 global_from_galley: TSTransform,
34 galley: &Galley,
35 ) -> Self {
36 let ccursor = cursor.into();
37 let pos = global_from_galley * pos_in_galley(galley, ccursor);
38 Self {
39 widget_id,
40 ccursor,
41 pos,
42 }
43 }
44}
45
46fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
47 galley.pos_from_cursor(ccursor).center()
48}
49
50impl core::fmt::Debug for WidgetTextCursor {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 let Self {
53 widget_id,
54 ccursor,
55 pos: _,
56 } = self;
57 f.debug_struct("WidgetTextCursor")
58 .field("widget_id", &widget_id.short_debug_format())
59 .field("ccursor", &ccursor.index)
60 .finish_non_exhaustive()
61 }
62}
63
64#[derive(Clone, Copy, Debug)]
65struct CurrentSelection {
66 pub layer_id: LayerId,
70
71 pub primary: WidgetTextCursor,
75
76 pub secondary: WidgetTextCursor,
79}
80
81#[derive(Clone, Debug, Default)]
85pub struct LabelSelectionState {
86 states: ViewportIdMap<ViewportLabelSelectionState>,
87}
88
89#[derive(Clone, Debug)]
91struct ViewportLabelSelectionState {
92 selection: Option<CurrentSelection>,
94
95 selection_bbox_last_frame: Rect,
96 selection_bbox_this_frame: Rect,
97
98 any_hovered: bool,
100
101 is_dragging: bool,
103
104 has_reached_primary: bool,
106
107 has_reached_secondary: bool,
109
110 text_to_copy: String,
112 last_copied_galley_rect: Option<Rect>,
113
114 painted_selections: Vec<(ShapeIdx, Vec<RowVertexIndices>)>,
118}
119
120impl Default for ViewportLabelSelectionState {
121 fn default() -> Self {
122 Self {
123 selection: Default::default(),
124 selection_bbox_last_frame: Rect::NOTHING,
125 selection_bbox_this_frame: Rect::NOTHING,
126 any_hovered: Default::default(),
127 is_dragging: Default::default(),
128 has_reached_primary: Default::default(),
129 has_reached_secondary: Default::default(),
130 text_to_copy: Default::default(),
131 last_copied_galley_rect: Default::default(),
132 painted_selections: Default::default(),
133 }
134 }
135}
136
137impl Plugin for LabelSelectionState {
138 fn debug_name(&self) -> &'static str {
139 "LabelSelectionState"
140 }
141
142 fn on_begin_pass(&mut self, ui: &mut Ui) {
143 self.states
144 .entry(ui.ctx().viewport_id())
145 .or_default()
146 .on_begin_pass(ui);
147 }
148
149 fn on_end_pass(&mut self, ui: &mut Ui) {
150 let viewport_id = ui.ctx().viewport_id();
151 let state = self.states.entry(viewport_id).or_default();
152 state.on_end_pass(ui);
153 if !state.is_active() {
154 self.states.remove(&viewport_id);
155 }
156 }
157}
158
159impl LabelSelectionState {
160 pub fn has_selection(&self) -> bool {
162 self.states
163 .values()
164 .any(ViewportLabelSelectionState::has_selection)
165 }
166
167 pub fn clear_selection(&mut self) {
169 self.states.clear();
170 }
171
172 pub fn label_text_selection(
175 ui: &Ui,
176 response: &Response,
177 galley_pos: Pos2,
178 mut galley: Arc<Galley>,
179 fallback_color: epaint::Color32,
180 underline: epaint::Stroke,
181 ) {
182 let plugin = ui.ctx().plugin::<Self>();
183 let mut plugin = plugin.lock();
184 let state = plugin.states.entry(ui.ctx().viewport_id()).or_default();
185 let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
186
187 let shape_idx = ui.painter().add(
188 epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
189 );
190
191 if !new_vertex_indices.is_empty() {
192 state
193 .painted_selections
194 .push((shape_idx, new_vertex_indices));
195 }
196 }
197}
198
199impl ViewportLabelSelectionState {
200 fn on_begin_pass(&mut self, ui: &Ui) {
201 if ui.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
202 }
205
206 self.selection_bbox_last_frame = self.selection_bbox_this_frame;
207 self.selection_bbox_this_frame = Rect::NOTHING;
208
209 self.any_hovered = false;
210 self.has_reached_primary = false;
211 self.has_reached_secondary = false;
212 self.text_to_copy.clear();
213 self.last_copied_galley_rect = None;
214 self.painted_selections.clear();
215 }
216
217 fn on_end_pass(&mut self, ui: &Ui) {
218 if self.is_dragging {
219 ui.set_cursor_icon(CursorIcon::Text);
220 }
221
222 if !self.has_reached_primary || !self.has_reached_secondary {
223 let prev_selection = self.selection.take();
228 if let Some(selection) = prev_selection {
229 ui.graphics_mut(|layers| {
232 if let Some(list) = layers.get_mut(selection.layer_id) {
233 for (shape_idx, row_selections) in self.painted_selections.drain(..) {
234 list.mutate_shape(shape_idx, |shape| {
235 if let epaint::Shape::Text(text_shape) = &mut shape.shape {
236 let galley = Arc::make_mut(&mut text_shape.galley);
237 for row_selection in row_selections {
238 if let Some(placed_row) =
239 galley.rows.get_mut(row_selection.row)
240 {
241 let row = Arc::make_mut(&mut placed_row.row);
242 for vertex_index in row_selection.vertex_indices {
243 if let Some(vertex) = row
244 .visuals
245 .mesh
246 .vertices
247 .get_mut(vertex_index as usize)
248 {
249 vertex.color = epaint::Color32::TRANSPARENT;
250 }
251 }
252 }
253 }
254 }
255 });
256 }
257 }
258 });
259 }
260 }
261
262 let pressed_escape = ui.input(|i| i.key_pressed(crate::Key::Escape));
263 let clicked_something_else = ui.input(|i| i.pointer.any_pressed()) && !self.any_hovered;
264 let delected_everything = pressed_escape || clicked_something_else;
265
266 if delected_everything {
267 self.selection = None;
268 }
269
270 if ui.input(|i| i.pointer.any_released()) {
271 self.is_dragging = false;
272 }
273
274 let text_to_copy = core::mem::take(&mut self.text_to_copy);
275 if !text_to_copy.is_empty() {
276 ui.copy_text(text_to_copy);
277 }
278 }
279
280 fn is_active(&self) -> bool {
281 self.selection.is_some() || self.is_dragging
282 }
283
284 fn has_selection(&self) -> bool {
285 self.selection.is_some()
286 }
287
288 fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) {
289 let new_text = selected_text(galley, cursor_range);
290 if new_text.is_empty() {
291 return;
292 }
293
294 if self.text_to_copy.is_empty() {
295 self.text_to_copy = new_text;
296 self.last_copied_galley_rect = Some(new_galley_rect);
297 return;
298 }
299
300 let Some(last_copied_galley_rect) = self.last_copied_galley_rect else {
301 self.text_to_copy = new_text;
302 self.last_copied_galley_rect = Some(new_galley_rect);
303 return;
304 };
305
306 if last_copied_galley_rect.bottom() <= new_galley_rect.top() {
310 self.text_to_copy.push('\n');
311 let vertical_distance = new_galley_rect.top() - last_copied_galley_rect.bottom();
312 if estimate_row_height(galley) * 0.5 < vertical_distance {
313 self.text_to_copy.push('\n');
314 }
315 } else {
316 let existing_ends_with_space =
317 self.text_to_copy.chars().last().map(|c| c.is_whitespace());
318
319 let new_text_starts_with_space_or_punctuation = new_text
320 .chars()
321 .next()
322 .is_some_and(|c| c.is_whitespace() || c.is_ascii_punctuation());
323
324 if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
325 {
326 self.text_to_copy.push(' ');
327 }
328 }
329
330 self.text_to_copy.push_str(&new_text);
331 self.last_copied_galley_rect = Some(new_galley_rect);
332 }
333
334 fn cursor_for(
335 &mut self,
336 ui: &Ui,
337 response: &Response,
338 global_from_galley: TSTransform,
339 galley: &Galley,
340 ) -> TextCursorState {
341 let Some(selection) = &mut self.selection else {
342 return TextCursorState::default();
344 };
345
346 if selection.layer_id != response.layer_id {
347 return TextCursorState::default();
349 }
350
351 let galley_from_global = global_from_galley.inverse();
352
353 let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;
354
355 let may_select_widget =
356 multi_widget_text_select || selection.primary.widget_id == response.id;
357
358 if self.is_dragging
359 && may_select_widget
360 && let Some(pointer_pos) = ui.ctx().pointer_interact_pos()
361 {
362 let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
363 let galley_rect = galley_rect.intersect(ui.clip_rect());
364
365 let is_in_same_column = galley_rect
366 .x_range()
367 .intersects(self.selection_bbox_last_frame.x_range());
368
369 let has_reached_primary =
370 self.has_reached_primary || response.id == selection.primary.widget_id;
371 let has_reached_secondary =
372 self.has_reached_secondary || response.id == selection.secondary.widget_id;
373
374 let new_primary = if response.contains_pointer() {
375 Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()))
377 } else if is_in_same_column
378 && !self.has_reached_primary
379 && selection.primary.pos.y <= selection.secondary.pos.y
380 && pointer_pos.y <= galley_rect.top()
381 && galley_rect.top() <= selection.secondary.pos.y
382 {
383 if DEBUG {
385 ui.ctx()
386 .debug_text(format!("Upwards drag; include {:?}", response.id));
387 }
388 Some(galley.begin())
389 } else if is_in_same_column
390 && has_reached_secondary
391 && has_reached_primary
392 && selection.secondary.pos.y <= selection.primary.pos.y
393 && selection.secondary.pos.y <= galley_rect.bottom()
394 && galley_rect.bottom() <= pointer_pos.y
395 {
396 if DEBUG {
400 ui.ctx()
401 .debug_text(format!("Downwards drag; include {:?}", response.id));
402 }
403 Some(galley.end())
404 } else {
405 None
406 };
407
408 if let Some(new_primary) = new_primary {
409 selection.primary =
410 WidgetTextCursor::new(response.id, new_primary, global_from_galley, galley);
411
412 let drag_started = ui.input(|i| i.pointer.any_pressed());
414 if drag_started {
415 if selection.layer_id == response.layer_id {
416 if ui.input(|i| i.modifiers.shift) {
417 } else {
419 selection.secondary = selection.primary;
421 }
422 } else {
423 selection.layer_id = response.layer_id;
425 selection.secondary = selection.primary;
426 }
427 }
428 }
429 }
430
431 let has_primary = response.id == selection.primary.widget_id;
432 let has_secondary = response.id == selection.secondary.widget_id;
433
434 if has_primary {
435 selection.primary.pos =
436 global_from_galley * pos_in_galley(galley, selection.primary.ccursor);
437 }
438 if has_secondary {
439 selection.secondary.pos =
440 global_from_galley * pos_in_galley(galley, selection.secondary.ccursor);
441 }
442
443 self.has_reached_primary |= has_primary;
444 self.has_reached_secondary |= has_secondary;
445
446 let primary = has_primary.then_some(selection.primary.ccursor);
447 let secondary = has_secondary.then_some(selection.secondary.ccursor);
448
449 match (primary, secondary) {
455 (Some(primary), Some(secondary)) => {
456 TextCursorState::from(CCursorRange {
458 primary,
459 secondary,
460 h_pos: None,
461 })
462 }
463
464 (Some(primary), None) => {
465 let secondary = if self.has_reached_secondary {
467 galley.begin()
471 } else {
472 galley.end()
474 };
475 TextCursorState::from(CCursorRange {
476 primary,
477 secondary,
478 h_pos: None,
479 })
480 }
481
482 (None, Some(secondary)) => {
483 let primary = if self.has_reached_primary {
485 galley.begin()
489 } else {
490 galley.end()
492 };
493 TextCursorState::from(CCursorRange {
494 primary,
495 secondary,
496 h_pos: None,
497 })
498 }
499
500 (None, None) => {
501 let is_in_middle = self.has_reached_primary != self.has_reached_secondary;
503 if is_in_middle {
504 if DEBUG {
505 response.ctx.debug_text(format!(
506 "widget in middle: {:?}, between {:?} and {:?}",
507 response.id, selection.primary.widget_id, selection.secondary.widget_id,
508 ));
509 }
510 TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
512 } else {
513 TextCursorState::default()
515 }
516 }
517 }
518 }
519
520 fn on_label(
522 &mut self,
523 ui: &Ui,
524 response: &Response,
525 galley_pos_in_layer: Pos2,
526 galley: &mut Arc<Galley>,
527 ) -> Vec<RowVertexIndices> {
528 let widget_id = response.id;
529
530 let global_from_layer = ui
531 .ctx()
532 .layer_transform_to_global(ui.layer_id())
533 .unwrap_or_default();
534 let layer_from_galley = TSTransform::from_translation(galley_pos_in_layer.to_vec2());
535 let galley_from_layer = layer_from_galley.inverse();
536 let layer_from_global = global_from_layer.inverse();
537 let galley_from_global = galley_from_layer * layer_from_global;
538 let global_from_galley = global_from_layer * layer_from_galley;
539
540 if response.hovered() {
541 ui.set_cursor_icon(CursorIcon::Text);
542 }
543
544 self.any_hovered |= response.hovered();
545 self.is_dragging |= response.is_pointer_button_down_on(); let old_selection = self.selection;
548
549 let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
550
551 let old_range = cursor_state.range(galley);
552
553 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos()
554 && response.contains_pointer()
555 {
556 let cursor_at_pointer =
557 galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2());
558
559 let dragged = false;
562 cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);
563 }
564
565 if let Some(mut cursor_range) = cursor_state.range(galley) {
566 let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
567 self.selection_bbox_this_frame |= galley_rect;
568
569 if let Some(selection) = &self.selection
570 && selection.primary.widget_id == response.id
571 {
572 process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
573 }
574
575 if got_copy_event(ui.ctx()) {
576 self.copy_text(galley_rect, galley, &cursor_range);
577 }
578
579 cursor_state.set_char_range(Some(cursor_range));
580 }
581
582 let new_range = cursor_state.range(galley);
584 let selection_changed = old_range != new_range;
585
586 if let (true, Some(range)) = (selection_changed, new_range) {
587 if let Some(selection) = &mut self.selection {
591 let primary_changed = Some(range.primary) != old_range.map(|r| r.primary);
592 let secondary_changed = Some(range.secondary) != old_range.map(|r| r.secondary);
593
594 selection.layer_id = response.layer_id;
595
596 if primary_changed || !ui.style().interaction.multi_widget_text_select {
597 selection.primary =
598 WidgetTextCursor::new(widget_id, range.primary, global_from_galley, galley);
599 self.has_reached_primary = true;
600 }
601 if secondary_changed || !ui.style().interaction.multi_widget_text_select {
602 selection.secondary = WidgetTextCursor::new(
603 widget_id,
604 range.secondary,
605 global_from_galley,
606 galley,
607 );
608 self.has_reached_secondary = true;
609 }
610 } else {
611 self.selection = Some(CurrentSelection {
613 layer_id: response.layer_id,
614 primary: WidgetTextCursor::new(
615 widget_id,
616 range.primary,
617 global_from_galley,
618 galley,
619 ),
620 secondary: WidgetTextCursor::new(
621 widget_id,
622 range.secondary,
623 global_from_galley,
624 galley,
625 ),
626 });
627 self.has_reached_primary = true;
628 self.has_reached_secondary = true;
629 }
630 }
631
632 if let Some(range) = new_range {
634 let old_primary = old_selection.map(|s| s.primary);
635 let new_primary = self.selection.as_ref().map(|s| s.primary);
636 if let Some(new_primary) = new_primary {
637 let primary_changed = old_primary.is_none_or(|old| {
638 old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
639 });
640 if primary_changed && new_primary.widget_id == widget_id {
641 let is_fully_visible = ui.clip_rect().contains_rect(response.rect); if selection_changed && !is_fully_visible {
643 let row_height = estimate_row_height(galley);
645 let primary_cursor_rect =
646 global_from_galley * cursor_rect(galley, &range.primary, row_height);
647 ui.scroll_to_rect(primary_cursor_rect, None);
648 }
649 }
650 }
651 }
652
653 let cursor_range = cursor_state.range(galley);
654
655 let mut new_vertex_indices = vec![];
656
657 if let Some(cursor_range) = cursor_range {
658 paint_text_selection(
659 galley,
660 ui.visuals(),
661 &cursor_range,
662 Some(&mut new_vertex_indices),
663 );
664 }
665
666 super::accesskit_text::update_accesskit_for_text_widget(
667 ui.ctx(),
668 response.id,
669 cursor_range,
670 accesskit::Role::Label,
671 global_from_galley,
672 galley,
673 );
674
675 new_vertex_indices
676 }
677}
678
679fn got_copy_event(ctx: &Context) -> bool {
680 ctx.input(|i| {
681 i.events
682 .iter()
683 .any(|e| matches!(e, Event::Copy | Event::Cut))
684 })
685}
686
687fn process_selection_key_events(
689 ctx: &Context,
690 galley: &Galley,
691 widget_id: Id,
692 cursor_range: &mut CCursorRange,
693) -> bool {
694 let os = ctx.os();
695
696 let mut changed = false;
697
698 ctx.input(|i| {
699 for event in &i.events {
702 changed |= cursor_range.on_event(os, event, galley, widget_id);
703 }
704 });
705
706 changed
707}
708
709fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String {
710 let everything_is_selected = cursor_range.contains(CCursorRange::select_all(galley));
713
714 let copy_everything = cursor_range.is_empty() || everything_is_selected;
715
716 if copy_everything {
717 galley.text().to_owned()
718 } else {
719 cursor_range.slice_str(galley).to_owned()
720 }
721}
722
723fn estimate_row_height(galley: &Galley) -> f32 {
724 if let Some(placed_row) = galley.rows.first() {
725 placed_row.height()
726 } else {
727 galley.size().y
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734 use crate::{RawInput, ViewportId, ViewportInfo};
735
736 fn child_viewport_input(viewport_id: ViewportId) -> RawInput {
737 let mut input = RawInput {
738 viewport_id,
739 ..Default::default()
740 };
741 input.viewports.insert(
742 viewport_id,
743 ViewportInfo {
744 parent: Some(ViewportId::ROOT),
745 ..Default::default()
746 },
747 );
748 input
749 }
750
751 fn test_selection() -> CurrentSelection {
752 let cursor = WidgetTextCursor {
753 widget_id: Id::new("selected_label"),
754 ccursor: CCursor::default(),
755 pos: Pos2::ZERO,
756 };
757 CurrentSelection {
758 layer_id: LayerId::background(),
759 primary: cursor,
760 secondary: cursor,
761 }
762 }
763
764 #[test]
765 fn viewport_passes_only_clean_up_their_own_label_selection() {
766 let ctx = Context::default();
767 let child_viewport_id = ViewportId::from_hash_of("child_viewport");
768 let plugin = ctx.plugin::<LabelSelectionState>();
769 plugin
770 .lock()
771 .states
772 .entry(child_viewport_id)
773 .or_default()
774 .selection = Some(test_selection());
775
776 let output = ctx.run_ui(RawInput::default(), |_| {});
777 assert!(
778 plugin
779 .lock()
780 .states
781 .get(&child_viewport_id)
782 .is_some_and(ViewportLabelSelectionState::has_selection),
783 "a pass in another viewport must not clear the child viewport selection"
784 );
785 output.drop_without_applying_deltas();
786
787 let output = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
788 assert!(
789 !plugin.lock().has_selection(),
790 "the selection must be cleared when its labels disappear from the same viewport"
791 );
792 output.drop_without_applying_deltas();
793 }
794}