1use std::sync::Arc;
2
3use emath::TSTransform;
4
5use crate::{
6 layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
7 Galley, Id, LayerId, Pos2, Rect, Response, Ui,
8};
9
10use super::{
11 text_cursor_state::cursor_rect,
12 visuals::{paint_text_selection, RowVertexIndices},
13 CursorRange, TextCursorState,
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_ccursor(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 LabelSelectionState {
127 pub(crate) fn register(ctx: &Context) {
128 ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass));
129 ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass));
130 }
131
132 pub fn load(ctx: &Context) -> Self {
133 let id = Id::new(ctx.viewport_id());
134 ctx.data(|data| data.get_temp::<Self>(id))
135 .unwrap_or_default()
136 }
137
138 pub fn store(self, ctx: &Context) {
139 let id = Id::new(ctx.viewport_id());
140 ctx.data_mut(|data| {
141 data.insert_temp(id, self);
142 });
143 }
144
145 fn begin_pass(ctx: &Context) {
146 let mut state = Self::load(ctx);
147
148 if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
149 }
152
153 state.selection_bbox_last_frame = state.selection_bbox_this_frame;
154 state.selection_bbox_this_frame = Rect::NOTHING;
155
156 state.any_hovered = false;
157 state.has_reached_primary = false;
158 state.has_reached_secondary = false;
159 state.text_to_copy.clear();
160 state.last_copied_galley_rect = None;
161 state.painted_selections.clear();
162
163 state.store(ctx);
164 }
165
166 fn end_pass(ctx: &Context) {
167 let mut state = Self::load(ctx);
168
169 if state.is_dragging {
170 ctx.set_cursor_icon(CursorIcon::Text);
171 }
172
173 if !state.has_reached_primary || !state.has_reached_secondary {
174 let prev_selection = state.selection.take();
179 if let Some(selection) = prev_selection {
180 ctx.graphics_mut(|layers| {
183 if let Some(list) = layers.get_mut(selection.layer_id) {
184 for (shape_idx, row_selections) in state.painted_selections.drain(..) {
185 list.mutate_shape(shape_idx, |shape| {
186 if let epaint::Shape::Text(text_shape) = &mut shape.shape {
187 let galley = Arc::make_mut(&mut text_shape.galley);
188 for row_selection in row_selections {
189 if let Some(row) = galley.rows.get_mut(row_selection.row) {
190 for vertex_index in row_selection.vertex_indices {
191 if let Some(vertex) = row
192 .visuals
193 .mesh
194 .vertices
195 .get_mut(vertex_index as usize)
196 {
197 vertex.color = epaint::Color32::TRANSPARENT;
198 }
199 }
200 }
201 }
202 }
203 });
204 }
205 }
206 });
207 }
208 }
209
210 let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape));
211 let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !state.any_hovered;
212 let delected_everything = pressed_escape || clicked_something_else;
213
214 if delected_everything {
215 state.selection = None;
216 }
217
218 if ctx.input(|i| i.pointer.any_released()) {
219 state.is_dragging = false;
220 }
221
222 let text_to_copy = std::mem::take(&mut state.text_to_copy);
223 if !text_to_copy.is_empty() {
224 ctx.copy_text(text_to_copy);
225 }
226
227 state.store(ctx);
228 }
229
230 pub fn has_selection(&self) -> bool {
231 self.selection.is_some()
232 }
233
234 pub fn clear_selection(&mut self) {
235 self.selection = None;
236 }
237
238 fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CursorRange) {
239 let new_text = selected_text(galley, cursor_range);
240 if new_text.is_empty() {
241 return;
242 }
243
244 if self.text_to_copy.is_empty() {
245 self.text_to_copy = new_text;
246 self.last_copied_galley_rect = Some(new_galley_rect);
247 return;
248 }
249
250 let Some(last_copied_galley_rect) = self.last_copied_galley_rect else {
251 self.text_to_copy = new_text;
252 self.last_copied_galley_rect = Some(new_galley_rect);
253 return;
254 };
255
256 if last_copied_galley_rect.bottom() <= new_galley_rect.top() {
260 self.text_to_copy.push('\n');
261 let vertical_distance = new_galley_rect.top() - last_copied_galley_rect.bottom();
262 if estimate_row_height(galley) * 0.5 < vertical_distance {
263 self.text_to_copy.push('\n');
264 }
265 } else {
266 let existing_ends_with_space =
267 self.text_to_copy.chars().last().map(|c| c.is_whitespace());
268
269 let new_text_starts_with_space_or_punctuation = new_text
270 .chars()
271 .next()
272 .is_some_and(|c| c.is_whitespace() || c.is_ascii_punctuation());
273
274 if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
275 {
276 self.text_to_copy.push(' ');
277 }
278 }
279
280 self.text_to_copy.push_str(&new_text);
281 self.last_copied_galley_rect = Some(new_galley_rect);
282 }
283
284 pub fn label_text_selection(
290 ui: &Ui,
291 response: &Response,
292 galley_pos: Pos2,
293 mut galley: Arc<Galley>,
294 fallback_color: epaint::Color32,
295 underline: epaint::Stroke,
296 ) {
297 let mut state = Self::load(ui.ctx());
298 let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
299
300 let shape_idx = ui.painter().add(
301 epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
302 );
303
304 if !new_vertex_indices.is_empty() {
305 state
306 .painted_selections
307 .push((shape_idx, new_vertex_indices));
308 }
309
310 state.store(ui.ctx());
311 }
312
313 fn cursor_for(
314 &mut self,
315 ui: &Ui,
316 response: &Response,
317 global_from_galley: TSTransform,
318 galley: &Galley,
319 ) -> TextCursorState {
320 let Some(selection) = &mut self.selection else {
321 return TextCursorState::default();
323 };
324
325 if selection.layer_id != response.layer_id {
326 return TextCursorState::default();
328 }
329
330 let galley_from_global = global_from_galley.inverse();
331
332 let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;
333
334 let may_select_widget =
335 multi_widget_text_select || selection.primary.widget_id == response.id;
336
337 if self.is_dragging && may_select_widget {
338 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
339 let galley_rect =
340 global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
341 let galley_rect = galley_rect.intersect(ui.clip_rect());
342
343 let is_in_same_column = galley_rect
344 .x_range()
345 .intersects(self.selection_bbox_last_frame.x_range());
346
347 let has_reached_primary =
348 self.has_reached_primary || response.id == selection.primary.widget_id;
349 let has_reached_secondary =
350 self.has_reached_secondary || response.id == selection.secondary.widget_id;
351
352 let new_primary = if response.contains_pointer() {
353 Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()))
355 } else if is_in_same_column
356 && !self.has_reached_primary
357 && selection.primary.pos.y <= selection.secondary.pos.y
358 && pointer_pos.y <= galley_rect.top()
359 && galley_rect.top() <= selection.secondary.pos.y
360 {
361 if DEBUG {
363 ui.ctx()
364 .debug_text(format!("Upwards drag; include {:?}", response.id));
365 }
366 Some(galley.begin())
367 } else if is_in_same_column
368 && has_reached_secondary
369 && has_reached_primary
370 && selection.secondary.pos.y <= selection.primary.pos.y
371 && selection.secondary.pos.y <= galley_rect.bottom()
372 && galley_rect.bottom() <= pointer_pos.y
373 {
374 if DEBUG {
378 ui.ctx()
379 .debug_text(format!("Downwards drag; include {:?}", response.id));
380 }
381 Some(galley.end())
382 } else {
383 None
384 };
385
386 if let Some(new_primary) = new_primary {
387 selection.primary =
388 WidgetTextCursor::new(response.id, new_primary, global_from_galley, galley);
389
390 let drag_started = ui.input(|i| i.pointer.any_pressed());
392 if drag_started {
393 if selection.layer_id == response.layer_id {
394 if ui.input(|i| i.modifiers.shift) {
395 } else {
397 selection.secondary = selection.primary;
399 }
400 } else {
401 selection.layer_id = response.layer_id;
403 selection.secondary = selection.primary;
404 }
405 }
406 }
407 }
408 }
409
410 let has_primary = response.id == selection.primary.widget_id;
411 let has_secondary = response.id == selection.secondary.widget_id;
412
413 if has_primary {
414 selection.primary.pos =
415 global_from_galley * pos_in_galley(galley, selection.primary.ccursor);
416 }
417 if has_secondary {
418 selection.secondary.pos =
419 global_from_galley * pos_in_galley(galley, selection.secondary.ccursor);
420 }
421
422 self.has_reached_primary |= has_primary;
423 self.has_reached_secondary |= has_secondary;
424
425 let primary = has_primary.then_some(selection.primary.ccursor);
426 let secondary = has_secondary.then_some(selection.secondary.ccursor);
427
428 match (primary, secondary) {
434 (Some(primary), Some(secondary)) => {
435 TextCursorState::from(CCursorRange { primary, secondary })
437 }
438
439 (Some(primary), None) => {
440 let secondary = if self.has_reached_secondary {
442 galley.begin().ccursor
446 } else {
447 galley.end().ccursor
449 };
450 TextCursorState::from(CCursorRange { primary, secondary })
451 }
452
453 (None, Some(secondary)) => {
454 let primary = if self.has_reached_primary {
456 galley.begin().ccursor
460 } else {
461 galley.end().ccursor
463 };
464 TextCursorState::from(CCursorRange { primary, secondary })
465 }
466
467 (None, None) => {
468 let is_in_middle = self.has_reached_primary != self.has_reached_secondary;
470 if is_in_middle {
471 if DEBUG {
472 response.ctx.debug_text(format!(
473 "widget in middle: {:?}, between {:?} and {:?}",
474 response.id, selection.primary.widget_id, selection.secondary.widget_id,
475 ));
476 }
477 TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
479 } else {
480 TextCursorState::default()
482 }
483 }
484 }
485 }
486
487 fn on_label(
489 &mut self,
490 ui: &Ui,
491 response: &Response,
492 galley_pos_in_layer: Pos2,
493 galley: &mut Arc<Galley>,
494 ) -> Vec<RowVertexIndices> {
495 let widget_id = response.id;
496
497 let global_from_layer = ui
498 .ctx()
499 .layer_transform_to_global(ui.layer_id())
500 .unwrap_or_default();
501 let layer_from_galley = TSTransform::from_translation(galley_pos_in_layer.to_vec2());
502 let galley_from_layer = layer_from_galley.inverse();
503 let layer_from_global = global_from_layer.inverse();
504 let galley_from_global = galley_from_layer * layer_from_global;
505 let global_from_galley = global_from_layer * layer_from_galley;
506
507 if response.hovered() {
508 ui.ctx().set_cursor_icon(CursorIcon::Text);
509 }
510
511 self.any_hovered |= response.hovered();
512 self.is_dragging |= response.is_pointer_button_down_on(); let old_selection = self.selection;
515
516 let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);
517
518 let old_range = cursor_state.range(galley);
519
520 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
521 if response.contains_pointer() {
522 let cursor_at_pointer =
523 galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2());
524
525 let dragged = false;
528 cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);
529 }
530 }
531
532 if let Some(mut cursor_range) = cursor_state.range(galley) {
533 let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
534 self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
535
536 if let Some(selection) = &self.selection {
537 if selection.primary.widget_id == response.id {
538 process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
539 }
540 }
541
542 if got_copy_event(ui.ctx()) {
543 self.copy_text(galley_rect, galley, &cursor_range);
544 }
545
546 cursor_state.set_range(Some(cursor_range));
547 }
548
549 let new_range = cursor_state.range(galley);
551 let selection_changed = old_range != new_range;
552
553 if let (true, Some(range)) = (selection_changed, new_range) {
554 if let Some(selection) = &mut self.selection {
558 let primary_changed = Some(range.primary) != old_range.map(|r| r.primary);
559 let secondary_changed = Some(range.secondary) != old_range.map(|r| r.secondary);
560
561 selection.layer_id = response.layer_id;
562
563 if primary_changed || !ui.style().interaction.multi_widget_text_select {
564 selection.primary =
565 WidgetTextCursor::new(widget_id, range.primary, global_from_galley, galley);
566 self.has_reached_primary = true;
567 }
568 if secondary_changed || !ui.style().interaction.multi_widget_text_select {
569 selection.secondary = WidgetTextCursor::new(
570 widget_id,
571 range.secondary,
572 global_from_galley,
573 galley,
574 );
575 self.has_reached_secondary = true;
576 }
577 } else {
578 self.selection = Some(CurrentSelection {
580 layer_id: response.layer_id,
581 primary: WidgetTextCursor::new(
582 widget_id,
583 range.primary,
584 global_from_galley,
585 galley,
586 ),
587 secondary: WidgetTextCursor::new(
588 widget_id,
589 range.secondary,
590 global_from_galley,
591 galley,
592 ),
593 });
594 self.has_reached_primary = true;
595 self.has_reached_secondary = true;
596 }
597 }
598
599 if let Some(range) = new_range {
601 let old_primary = old_selection.map(|s| s.primary);
602 let new_primary = self.selection.as_ref().map(|s| s.primary);
603 if let Some(new_primary) = new_primary {
604 let primary_changed = old_primary.map_or(true, |old| {
605 old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
606 });
607 if primary_changed && new_primary.widget_id == widget_id {
608 let is_fully_visible = ui.clip_rect().contains_rect(response.rect); if selection_changed && !is_fully_visible {
610 let row_height = estimate_row_height(galley);
612 let primary_cursor_rect =
613 global_from_galley * cursor_rect(galley, &range.primary, row_height);
614 ui.scroll_to_rect(primary_cursor_rect, None);
615 }
616 }
617 }
618 }
619
620 let cursor_range = cursor_state.range(galley);
621
622 let mut new_vertex_indices = vec![];
623
624 if let Some(cursor_range) = cursor_range {
625 paint_text_selection(
626 galley,
627 ui.visuals(),
628 &cursor_range,
629 Some(&mut new_vertex_indices),
630 );
631 }
632
633 #[cfg(feature = "accesskit")]
634 super::accesskit_text::update_accesskit_for_text_widget(
635 ui.ctx(),
636 response.id,
637 cursor_range,
638 accesskit::Role::Label,
639 global_from_galley,
640 galley,
641 );
642
643 new_vertex_indices
644 }
645}
646
647fn got_copy_event(ctx: &Context) -> bool {
648 ctx.input(|i| {
649 i.events
650 .iter()
651 .any(|e| matches!(e, Event::Copy | Event::Cut))
652 })
653}
654
655fn process_selection_key_events(
657 ctx: &Context,
658 galley: &Galley,
659 widget_id: Id,
660 cursor_range: &mut CursorRange,
661) -> bool {
662 let os = ctx.os();
663
664 let mut changed = false;
665
666 ctx.input(|i| {
667 for event in &i.events {
670 changed |= cursor_range.on_event(os, event, galley, widget_id);
671 }
672 });
673
674 changed
675}
676
677fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
678 let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley));
681
682 let copy_everything = cursor_range.is_empty() || everything_is_selected;
683
684 if copy_everything {
685 galley.text().to_owned()
686 } else {
687 cursor_range.slice_str(galley).to_owned()
688 }
689}
690
691fn estimate_row_height(galley: &Galley) -> f32 {
692 if let Some(row) = galley.rows.first() {
693 row.rect.height()
694 } else {
695 galley.size().y
696 }
697}