1use std::sync::Arc;
2
3use emath::TSTransform;
4
5use crate::{
6 Context, CursorIcon, Event, Galley, Id, LayerId, Plugin, Pos2, Rect, Response, Ui,
7 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 std::fmt::Debug for WidgetTextCursor {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("WidgetTextCursor")
53 .field("widget_id", &self.widget_id.short_debug_format())
54 .field("ccursor", &self.ccursor.index)
55 .finish()
56 }
57}
58
59#[derive(Clone, Copy, Debug)]
60struct CurrentSelection {
61 pub layer_id: LayerId,
65
66 pub primary: WidgetTextCursor,
70
71 pub secondary: WidgetTextCursor,
74}
75
76#[derive(Clone, Debug)]
80pub struct LabelSelectionState {
81 selection: Option<CurrentSelection>,
83
84 selection_bbox_last_frame: Rect,
85 selection_bbox_this_frame: Rect,
86
87 any_hovered: bool,
89
90 is_dragging: bool,
92
93 has_reached_primary: bool,
95
96 has_reached_secondary: bool,
98
99 text_to_copy: String,
101 last_copied_galley_rect: Option<Rect>,
102
103 painted_selections: Vec<(ShapeIdx, Vec<RowVertexIndices>)>,
107}
108
109impl Default for LabelSelectionState {
110 fn default() -> Self {
111 Self {
112 selection: Default::default(),
113 selection_bbox_last_frame: Rect::NOTHING,
114 selection_bbox_this_frame: Rect::NOTHING,
115 any_hovered: Default::default(),
116 is_dragging: Default::default(),
117 has_reached_primary: Default::default(),
118 has_reached_secondary: Default::default(),
119 text_to_copy: Default::default(),
120 last_copied_galley_rect: Default::default(),
121 painted_selections: Default::default(),
122 }
123 }
124}
125
126impl Plugin for LabelSelectionState {
127 fn debug_name(&self) -> &'static str {
128 "LabelSelectionState"
129 }
130
131 fn on_begin_pass(&mut self, ctx: &Context) {
132 if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
133 }
136
137 self.selection_bbox_last_frame = self.selection_bbox_this_frame;
138 self.selection_bbox_this_frame = Rect::NOTHING;
139
140 self.any_hovered = false;
141 self.has_reached_primary = false;
142 self.has_reached_secondary = false;
143 self.text_to_copy.clear();
144 self.last_copied_galley_rect = None;
145 self.painted_selections.clear();
146 }
147
148 fn on_end_pass(&mut self, ctx: &Context) {
149 if self.is_dragging {
150 ctx.set_cursor_icon(CursorIcon::Text);
151 }
152
153 if !self.has_reached_primary || !self.has_reached_secondary {
154 let prev_selection = self.selection.take();
159 if let Some(selection) = prev_selection {
160 ctx.graphics_mut(|layers| {
163 if let Some(list) = layers.get_mut(selection.layer_id) {
164 for (shape_idx, row_selections) in self.painted_selections.drain(..) {
165 list.mutate_shape(shape_idx, |shape| {
166 if let epaint::Shape::Text(text_shape) = &mut shape.shape {
167 let galley = Arc::make_mut(&mut text_shape.galley);
168 for row_selection in row_selections {
169 if let Some(placed_row) =
170 galley.rows.get_mut(row_selection.row)
171 {
172 let row = Arc::make_mut(&mut placed_row.row);
173 for vertex_index in row_selection.vertex_indices {
174 if let Some(vertex) = row
175 .visuals
176 .mesh
177 .vertices
178 .get_mut(vertex_index as usize)
179 {
180 vertex.color = epaint::Color32::TRANSPARENT;
181 }
182 }
183 }
184 }
185 }
186 });
187 }
188 }
189 });
190 }
191 }
192
193 let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape));
194 let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !self.any_hovered;
195 let delected_everything = pressed_escape || clicked_something_else;
196
197 if delected_everything {
198 self.selection = None;
199 }
200
201 if ctx.input(|i| i.pointer.any_released()) {
202 self.is_dragging = false;
203 }
204
205 let text_to_copy = std::mem::take(&mut self.text_to_copy);
206 if !text_to_copy.is_empty() {
207 ctx.copy_text(text_to_copy);
208 }
209 }
210}
211
212impl LabelSelectionState {
213 pub fn has_selection(&self) -> bool {
214 self.selection.is_some()
215 }
216
217 pub fn clear_selection(&mut self) {
218 self.selection = None;
219 }
220
221 fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CCursorRange) {
222 let new_text = selected_text(galley, cursor_range);
223 if new_text.is_empty() {
224 return;
225 }
226
227 if self.text_to_copy.is_empty() {
228 self.text_to_copy = new_text;
229 self.last_copied_galley_rect = Some(new_galley_rect);
230 return;
231 }
232
233 let Some(last_copied_galley_rect) = self.last_copied_galley_rect else {
234 self.text_to_copy = new_text;
235 self.last_copied_galley_rect = Some(new_galley_rect);
236 return;
237 };
238
239 if last_copied_galley_rect.bottom() <= new_galley_rect.top() {
243 self.text_to_copy.push('\n');
244 let vertical_distance = new_galley_rect.top() - last_copied_galley_rect.bottom();
245 if estimate_row_height(galley) * 0.5 < vertical_distance {
246 self.text_to_copy.push('\n');
247 }
248 } else {
249 let existing_ends_with_space =
250 self.text_to_copy.chars().last().map(|c| c.is_whitespace());
251
252 let new_text_starts_with_space_or_punctuation = new_text
253 .chars()
254 .next()
255 .is_some_and(|c| c.is_whitespace() || c.is_ascii_punctuation());
256
257 if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
258 {
259 self.text_to_copy.push(' ');
260 }
261 }
262
263 self.text_to_copy.push_str(&new_text);
264 self.last_copied_galley_rect = Some(new_galley_rect);
265 }
266
267 pub fn label_text_selection(
273 ui: &Ui,
274 response: &Response,
275 galley_pos: Pos2,
276 mut galley: Arc<Galley>,
277 fallback_color: epaint::Color32,
278 underline: epaint::Stroke,
279 ) {
280 let plugin = ui.ctx().plugin::<Self>();
281 let mut state = plugin.lock();
282 let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
283
284 let shape_idx = ui.painter().add(
285 epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
286 );
287
288 if !new_vertex_indices.is_empty() {
289 state
290 .painted_selections
291 .push((shape_idx, new_vertex_indices));
292 }
293 }
294
295 fn cursor_for(
296 &mut self,
297 ui: &Ui,
298 response: &Response,
299 global_from_galley: TSTransform,
300 galley: &Galley,
301 ) -> TextCursorState {
302 let Some(selection) = &mut self.selection else {
303 return TextCursorState::default();
305 };
306
307 if selection.layer_id != response.layer_id {
308 return TextCursorState::default();
310 }
311
312 let galley_from_global = global_from_galley.inverse();
313
314 let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;
315
316 let may_select_widget =
317 multi_widget_text_select || selection.primary.widget_id == response.id;
318
319 if self.is_dragging
320 && may_select_widget
321 && let Some(pointer_pos) = ui.ctx().pointer_interact_pos()
322 {
323 let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
324 let galley_rect = galley_rect.intersect(ui.clip_rect());
325
326 let is_in_same_column = galley_rect
327 .x_range()
328 .intersects(self.selection_bbox_last_frame.x_range());
329
330 let has_reached_primary =
331 self.has_reached_primary || response.id == selection.primary.widget_id;
332 let has_reached_secondary =
333 self.has_reached_secondary || response.id == selection.secondary.widget_id;
334
335 let new_primary = if response.contains_pointer() {
336 Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()))
338 } else if is_in_same_column
339 && !self.has_reached_primary
340 && selection.primary.pos.y <= selection.secondary.pos.y
341 && pointer_pos.y <= galley_rect.top()
342 && galley_rect.top() <= selection.secondary.pos.y
343 {
344 if DEBUG {
346 ui.ctx()
347 .debug_text(format!("Upwards drag; include {:?}", response.id));
348 }
349 Some(galley.begin())
350 } else if is_in_same_column
351 && has_reached_secondary
352 && has_reached_primary
353 && selection.secondary.pos.y <= selection.primary.pos.y
354 && selection.secondary.pos.y <= galley_rect.bottom()
355 && galley_rect.bottom() <= pointer_pos.y
356 {
357 if DEBUG {
361 ui.ctx()
362 .debug_text(format!("Downwards drag; include {:?}", response.id));
363 }
364 Some(galley.end())
365 } else {
366 None
367 };
368
369 if let Some(new_primary) = new_primary {
370 selection.primary =
371 WidgetTextCursor::new(response.id, new_primary, global_from_galley, galley);
372
373 let drag_started = ui.input(|i| i.pointer.any_pressed());
375 if drag_started {
376 if selection.layer_id == response.layer_id {
377 if ui.input(|i| i.modifiers.shift) {
378 } else {
380 selection.secondary = selection.primary;
382 }
383 } else {
384 selection.layer_id = response.layer_id;
386 selection.secondary = selection.primary;
387 }
388 }
389 }
390 }
391
392 let has_primary = response.id == selection.primary.widget_id;
393 let has_secondary = response.id == selection.secondary.widget_id;
394
395 if has_primary {
396 selection.primary.pos =
397 global_from_galley * pos_in_galley(galley, selection.primary.ccursor);
398 }
399 if has_secondary {
400 selection.secondary.pos =
401 global_from_galley * pos_in_galley(galley, selection.secondary.ccursor);
402 }
403
404 self.has_reached_primary |= has_primary;
405 self.has_reached_secondary |= has_secondary;
406
407 let primary = has_primary.then_some(selection.primary.ccursor);
408 let secondary = has_secondary.then_some(selection.secondary.ccursor);
409
410 match (primary, secondary) {
416 (Some(primary), Some(secondary)) => {
417 TextCursorState::from(CCursorRange {
419 primary,
420 secondary,
421 h_pos: None,
422 })
423 }
424
425 (Some(primary), None) => {
426 let secondary = if self.has_reached_secondary {
428 galley.begin()
432 } else {
433 galley.end()
435 };
436 TextCursorState::from(CCursorRange {
437 primary,
438 secondary,
439 h_pos: None,
440 })
441 }
442
443 (None, Some(secondary)) => {
444 let primary = if self.has_reached_primary {
446 galley.begin()
450 } else {
451 galley.end()
453 };
454 TextCursorState::from(CCursorRange {
455 primary,
456 secondary,
457 h_pos: None,
458 })
459 }
460
461 (None, None) => {
462 let is_in_middle = self.has_reached_primary != self.has_reached_secondary;
464 if is_in_middle {
465 if DEBUG {
466 response.ctx.debug_text(format!(
467 "widget in middle: {:?}, between {:?} and {:?}",
468 response.id, selection.primary.widget_id, selection.secondary.widget_id,
469 ));
470 }
471 TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
473 } else {
474 TextCursorState::default()
476 }
477 }
478 }
479 }
480
481 fn on_label(
483 &mut self,
484 ui: &Ui,
485 response: &Response,
486 galley_pos_in_layer: Pos2,
487 galley: &mut Arc<Galley>,
488 ) -> Vec<RowVertexIndices> {
489 let widget_id = response.id;
490
491 let global_from_layer = ui
492 .ctx()
493 .layer_transform_to_global(ui.layer_id())
494 .unwrap_or_default();
495 let layer_from_galley = TSTransform::from_translation(galley_pos_in_layer.to_vec2());
496 let galley_from_layer = layer_from_galley.inverse();
497 let layer_from_global = global_from_layer.inverse();
498 let galley_from_global = galley_from_layer * layer_from_global;
499 let global_from_galley = global_from_layer * layer_from_galley;
500
501 if response.hovered() {
502 ui.ctx().set_cursor_icon(CursorIcon::Text);
503 }
504
505 self.any_hovered |= response.hovered();
506 self.is_dragging |= response.is_pointer_button_down_on(); let old_selection = self.selection;
509
510 let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
511
512 let old_range = cursor_state.range(galley);
513
514 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos()
515 && response.contains_pointer()
516 {
517 let cursor_at_pointer =
518 galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2());
519
520 let dragged = false;
523 cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);
524 }
525
526 if let Some(mut cursor_range) = cursor_state.range(galley) {
527 let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
528 self.selection_bbox_this_frame |= galley_rect;
529
530 if let Some(selection) = &self.selection
531 && selection.primary.widget_id == response.id
532 {
533 process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
534 }
535
536 if got_copy_event(ui.ctx()) {
537 self.copy_text(galley_rect, galley, &cursor_range);
538 }
539
540 cursor_state.set_char_range(Some(cursor_range));
541 }
542
543 let new_range = cursor_state.range(galley);
545 let selection_changed = old_range != new_range;
546
547 if let (true, Some(range)) = (selection_changed, new_range) {
548 if let Some(selection) = &mut self.selection {
552 let primary_changed = Some(range.primary) != old_range.map(|r| r.primary);
553 let secondary_changed = Some(range.secondary) != old_range.map(|r| r.secondary);
554
555 selection.layer_id = response.layer_id;
556
557 if primary_changed || !ui.style().interaction.multi_widget_text_select {
558 selection.primary =
559 WidgetTextCursor::new(widget_id, range.primary, global_from_galley, galley);
560 self.has_reached_primary = true;
561 }
562 if secondary_changed || !ui.style().interaction.multi_widget_text_select {
563 selection.secondary = WidgetTextCursor::new(
564 widget_id,
565 range.secondary,
566 global_from_galley,
567 galley,
568 );
569 self.has_reached_secondary = true;
570 }
571 } else {
572 self.selection = Some(CurrentSelection {
574 layer_id: response.layer_id,
575 primary: WidgetTextCursor::new(
576 widget_id,
577 range.primary,
578 global_from_galley,
579 galley,
580 ),
581 secondary: WidgetTextCursor::new(
582 widget_id,
583 range.secondary,
584 global_from_galley,
585 galley,
586 ),
587 });
588 self.has_reached_primary = true;
589 self.has_reached_secondary = true;
590 }
591 }
592
593 if let Some(range) = new_range {
595 let old_primary = old_selection.map(|s| s.primary);
596 let new_primary = self.selection.as_ref().map(|s| s.primary);
597 if let Some(new_primary) = new_primary {
598 let primary_changed = old_primary.is_none_or(|old| {
599 old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
600 });
601 if primary_changed && new_primary.widget_id == widget_id {
602 let is_fully_visible = ui.clip_rect().contains_rect(response.rect); if selection_changed && !is_fully_visible {
604 let row_height = estimate_row_height(galley);
606 let primary_cursor_rect =
607 global_from_galley * cursor_rect(galley, &range.primary, row_height);
608 ui.scroll_to_rect(primary_cursor_rect, None);
609 }
610 }
611 }
612 }
613
614 let cursor_range = cursor_state.range(galley);
615
616 let mut new_vertex_indices = vec![];
617
618 if let Some(cursor_range) = cursor_range {
619 paint_text_selection(
620 galley,
621 ui.visuals(),
622 &cursor_range,
623 Some(&mut new_vertex_indices),
624 );
625 }
626
627 #[cfg(feature = "accesskit")]
628 super::accesskit_text::update_accesskit_for_text_widget(
629 ui.ctx(),
630 response.id,
631 cursor_range,
632 accesskit::Role::Label,
633 global_from_galley,
634 galley,
635 );
636
637 new_vertex_indices
638 }
639}
640
641fn got_copy_event(ctx: &Context) -> bool {
642 ctx.input(|i| {
643 i.events
644 .iter()
645 .any(|e| matches!(e, Event::Copy | Event::Cut))
646 })
647}
648
649fn process_selection_key_events(
651 ctx: &Context,
652 galley: &Galley,
653 widget_id: Id,
654 cursor_range: &mut CCursorRange,
655) -> bool {
656 let os = ctx.os();
657
658 let mut changed = false;
659
660 ctx.input(|i| {
661 for event in &i.events {
664 changed |= cursor_range.on_event(os, event, galley, widget_id);
665 }
666 });
667
668 changed
669}
670
671fn selected_text(galley: &Galley, cursor_range: &CCursorRange) -> String {
672 let everything_is_selected = cursor_range.contains(CCursorRange::select_all(galley));
675
676 let copy_everything = cursor_range.is_empty() || everything_is_selected;
677
678 if copy_everything {
679 galley.text().to_owned()
680 } else {
681 cursor_range.slice_str(galley).to_owned()
682 }
683}
684
685fn estimate_row_height(galley: &Galley) -> f32 {
686 if let Some(placed_row) = galley.rows.first() {
687 placed_row.height()
688 } else {
689 galley.size().y
690 }
691}