1use std::sync::Arc;
2
3use crate::{
4 layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
5 Galley, Id, LayerId, Pos2, Rect, Response, Ui,
6};
7
8use super::{
9 text_cursor_state::cursor_rect,
10 visuals::{paint_text_selection, RowVertexIndices},
11 CursorRange, TextCursorState,
12};
13
14const DEBUG: bool = false; #[derive(Clone, Copy)]
19struct WidgetTextCursor {
20 widget_id: Id,
21 ccursor: CCursor,
22
23 pos: Pos2,
25}
26
27impl WidgetTextCursor {
28 fn new(widget_id: Id, cursor: impl Into<CCursor>, galley_pos: Pos2, galley: &Galley) -> Self {
29 let ccursor = cursor.into();
30 let pos = pos_in_galley(galley_pos, galley, ccursor);
31 Self {
32 widget_id,
33 ccursor,
34 pos,
35 }
36 }
37}
38
39fn pos_in_galley(galley_pos: Pos2, galley: &Galley, ccursor: CCursor) -> Pos2 {
40 galley_pos + galley.pos_from_ccursor(ccursor).center().to_vec2()
41}
42
43impl std::fmt::Debug for WidgetTextCursor {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("WidgetTextCursor")
46 .field("widget_id", &self.widget_id.short_debug_format())
47 .field("ccursor", &self.ccursor.index)
48 .finish()
49 }
50}
51
52#[derive(Clone, Copy, Debug)]
53struct CurrentSelection {
54 pub layer_id: LayerId,
58
59 pub primary: WidgetTextCursor,
63
64 pub secondary: WidgetTextCursor,
67}
68
69#[derive(Clone, Debug)]
73pub struct LabelSelectionState {
74 selection: Option<CurrentSelection>,
76
77 selection_bbox_last_frame: Rect,
78 selection_bbox_this_frame: Rect,
79
80 any_hovered: bool,
82
83 is_dragging: bool,
85
86 has_reached_primary: bool,
88
89 has_reached_secondary: bool,
91
92 text_to_copy: String,
94 last_copied_galley_rect: Option<Rect>,
95
96 painted_selections: Vec<(ShapeIdx, Vec<RowVertexIndices>)>,
100}
101
102impl Default for LabelSelectionState {
103 fn default() -> Self {
104 Self {
105 selection: Default::default(),
106 selection_bbox_last_frame: Rect::NOTHING,
107 selection_bbox_this_frame: Rect::NOTHING,
108 any_hovered: Default::default(),
109 is_dragging: Default::default(),
110 has_reached_primary: Default::default(),
111 has_reached_secondary: Default::default(),
112 text_to_copy: Default::default(),
113 last_copied_galley_rect: Default::default(),
114 painted_selections: Default::default(),
115 }
116 }
117}
118
119impl LabelSelectionState {
120 pub(crate) fn register(ctx: &Context) {
121 ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass));
122 ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass));
123 }
124
125 pub fn load(ctx: &Context) -> Self {
126 let id = Id::new(ctx.viewport_id());
127 ctx.data(|data| data.get_temp::<Self>(id))
128 .unwrap_or_default()
129 }
130
131 pub fn store(self, ctx: &Context) {
132 let id = Id::new(ctx.viewport_id());
133 ctx.data_mut(|data| {
134 data.insert_temp(id, self);
135 });
136 }
137
138 fn begin_pass(ctx: &Context) {
139 let mut state = Self::load(ctx);
140
141 if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
142 }
145
146 state.selection_bbox_last_frame = state.selection_bbox_this_frame;
147 state.selection_bbox_this_frame = Rect::NOTHING;
148
149 state.any_hovered = false;
150 state.has_reached_primary = false;
151 state.has_reached_secondary = false;
152 state.text_to_copy.clear();
153 state.last_copied_galley_rect = None;
154 state.painted_selections.clear();
155
156 state.store(ctx);
157 }
158
159 fn end_pass(ctx: &Context) {
160 let mut state = Self::load(ctx);
161
162 if state.is_dragging {
163 ctx.set_cursor_icon(CursorIcon::Text);
164 }
165
166 if !state.has_reached_primary || !state.has_reached_secondary {
167 let prev_selection = state.selection.take();
172 if let Some(selection) = prev_selection {
173 ctx.graphics_mut(|layers| {
176 if let Some(list) = layers.get_mut(selection.layer_id) {
177 for (shape_idx, row_selections) in state.painted_selections.drain(..) {
178 list.mutate_shape(shape_idx, |shape| {
179 if let epaint::Shape::Text(text_shape) = &mut shape.shape {
180 let galley = Arc::make_mut(&mut text_shape.galley);
181 for row_selection in row_selections {
182 if let Some(row) = galley.rows.get_mut(row_selection.row) {
183 for vertex_index in row_selection.vertex_indices {
184 if let Some(vertex) = row
185 .visuals
186 .mesh
187 .vertices
188 .get_mut(vertex_index as usize)
189 {
190 vertex.color = epaint::Color32::TRANSPARENT;
191 }
192 }
193 }
194 }
195 }
196 });
197 }
198 }
199 });
200 }
201 }
202
203 let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape));
204 let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !state.any_hovered;
205 let delected_everything = pressed_escape || clicked_something_else;
206
207 if delected_everything {
208 state.selection = None;
209 }
210
211 if ctx.input(|i| i.pointer.any_released()) {
212 state.is_dragging = false;
213 }
214
215 let text_to_copy = std::mem::take(&mut state.text_to_copy);
216 if !text_to_copy.is_empty() {
217 ctx.copy_text(text_to_copy);
218 }
219
220 state.store(ctx);
221 }
222
223 pub fn has_selection(&self) -> bool {
224 self.selection.is_some()
225 }
226
227 pub fn clear_selection(&mut self) {
228 self.selection = None;
229 }
230
231 fn copy_text(&mut self, galley_pos: Pos2, galley: &Galley, cursor_range: &CursorRange) {
232 let new_galley_rect = Rect::from_min_size(galley_pos, galley.size());
233 let new_text = selected_text(galley, cursor_range);
234 if new_text.is_empty() {
235 return;
236 }
237
238 if self.text_to_copy.is_empty() {
239 self.text_to_copy = new_text;
240 self.last_copied_galley_rect = Some(new_galley_rect);
241 return;
242 }
243
244 let Some(last_copied_galley_rect) = self.last_copied_galley_rect else {
245 self.text_to_copy = new_text;
246 self.last_copied_galley_rect = Some(new_galley_rect);
247 return;
248 };
249
250 if last_copied_galley_rect.bottom() <= new_galley_rect.top() {
254 self.text_to_copy.push('\n');
255 let vertical_distance = new_galley_rect.top() - last_copied_galley_rect.bottom();
256 if estimate_row_height(galley) * 0.5 < vertical_distance {
257 self.text_to_copy.push('\n');
258 }
259 } else {
260 let existing_ends_with_space =
261 self.text_to_copy.chars().last().map(|c| c.is_whitespace());
262
263 let new_text_starts_with_space_or_punctuation = new_text
264 .chars()
265 .next()
266 .map_or(false, |c| c.is_whitespace() || c.is_ascii_punctuation());
267
268 if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
269 {
270 self.text_to_copy.push(' ');
271 }
272 }
273
274 self.text_to_copy.push_str(&new_text);
275 self.last_copied_galley_rect = Some(new_galley_rect);
276 }
277
278 pub fn label_text_selection(
284 ui: &Ui,
285 response: &Response,
286 galley_pos: Pos2,
287 mut galley: Arc<Galley>,
288 fallback_color: epaint::Color32,
289 underline: epaint::Stroke,
290 ) {
291 let mut state = Self::load(ui.ctx());
292 let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
293
294 let shape_idx = ui.painter().add(
295 epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
296 );
297
298 if !new_vertex_indices.is_empty() {
299 state
300 .painted_selections
301 .push((shape_idx, new_vertex_indices));
302 }
303
304 state.store(ui.ctx());
305 }
306
307 fn cursor_for(
308 &mut self,
309 ui: &Ui,
310 response: &Response,
311 galley_pos: Pos2,
312 galley: &Galley,
313 ) -> TextCursorState {
314 let Some(selection) = &mut self.selection else {
315 return TextCursorState::default();
317 };
318
319 if selection.layer_id != response.layer_id {
320 return TextCursorState::default();
322 }
323
324 let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;
325
326 let may_select_widget =
327 multi_widget_text_select || selection.primary.widget_id == response.id;
328
329 if self.is_dragging && may_select_widget {
330 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
331 let galley_rect = Rect::from_min_size(galley_pos, galley.size());
332 let galley_rect = galley_rect.intersect(ui.clip_rect());
333
334 let is_in_same_column = galley_rect
335 .x_range()
336 .intersects(self.selection_bbox_last_frame.x_range());
337
338 let has_reached_primary =
339 self.has_reached_primary || response.id == selection.primary.widget_id;
340 let has_reached_secondary =
341 self.has_reached_secondary || response.id == selection.secondary.widget_id;
342
343 let new_primary = if response.contains_pointer() {
344 Some(galley.cursor_from_pos(pointer_pos - galley_pos))
346 } else if is_in_same_column
347 && !self.has_reached_primary
348 && selection.primary.pos.y <= selection.secondary.pos.y
349 && pointer_pos.y <= galley_rect.top()
350 && galley_rect.top() <= selection.secondary.pos.y
351 {
352 if DEBUG {
354 ui.ctx()
355 .debug_text(format!("Upwards drag; include {:?}", response.id));
356 }
357 Some(galley.begin())
358 } else if is_in_same_column
359 && has_reached_secondary
360 && has_reached_primary
361 && selection.secondary.pos.y <= selection.primary.pos.y
362 && selection.secondary.pos.y <= galley_rect.bottom()
363 && galley_rect.bottom() <= pointer_pos.y
364 {
365 if DEBUG {
369 ui.ctx()
370 .debug_text(format!("Downwards drag; include {:?}", response.id));
371 }
372 Some(galley.end())
373 } else {
374 None
375 };
376
377 if let Some(new_primary) = new_primary {
378 selection.primary =
379 WidgetTextCursor::new(response.id, new_primary, galley_pos, galley);
380
381 let drag_started = ui.input(|i| i.pointer.any_pressed());
383 if drag_started {
384 if selection.layer_id == response.layer_id {
385 if ui.input(|i| i.modifiers.shift) {
386 } else {
388 selection.secondary = selection.primary;
390 }
391 } else {
392 selection.layer_id = response.layer_id;
394 selection.secondary = selection.primary;
395 }
396 }
397 }
398 }
399 }
400
401 let has_primary = response.id == selection.primary.widget_id;
402 let has_secondary = response.id == selection.secondary.widget_id;
403
404 if has_primary {
405 selection.primary.pos = pos_in_galley(galley_pos, galley, selection.primary.ccursor);
406 }
407 if has_secondary {
408 selection.secondary.pos =
409 pos_in_galley(galley_pos, galley, selection.secondary.ccursor);
410 }
411
412 self.has_reached_primary |= has_primary;
413 self.has_reached_secondary |= has_secondary;
414
415 let primary = has_primary.then_some(selection.primary.ccursor);
416 let secondary = has_secondary.then_some(selection.secondary.ccursor);
417
418 match (primary, secondary) {
424 (Some(primary), Some(secondary)) => {
425 TextCursorState::from(CCursorRange { primary, secondary })
427 }
428
429 (Some(primary), None) => {
430 let secondary = if self.has_reached_secondary {
432 galley.begin().ccursor
436 } else {
437 galley.end().ccursor
439 };
440 TextCursorState::from(CCursorRange { primary, secondary })
441 }
442
443 (None, Some(secondary)) => {
444 let primary = if self.has_reached_primary {
446 galley.begin().ccursor
450 } else {
451 galley.end().ccursor
453 };
454 TextCursorState::from(CCursorRange { primary, secondary })
455 }
456
457 (None, None) => {
458 let is_in_middle = self.has_reached_primary != self.has_reached_secondary;
460 if is_in_middle {
461 if DEBUG {
462 response.ctx.debug_text(format!(
463 "widget in middle: {:?}, between {:?} and {:?}",
464 response.id, selection.primary.widget_id, selection.secondary.widget_id,
465 ));
466 }
467 TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
469 } else {
470 TextCursorState::default()
472 }
473 }
474 }
475 }
476
477 fn on_label(
479 &mut self,
480 ui: &Ui,
481 response: &Response,
482 galley_pos: Pos2,
483 galley: &mut Arc<Galley>,
484 ) -> Vec<RowVertexIndices> {
485 let widget_id = response.id;
486
487 if response.hovered {
488 ui.ctx().set_cursor_icon(CursorIcon::Text);
489 }
490
491 self.any_hovered |= response.hovered();
492 self.is_dragging |= response.is_pointer_button_down_on(); let old_selection = self.selection;
495
496 let mut cursor_state = self.cursor_for(ui, response, galley_pos, galley);
497
498 let old_range = cursor_state.range(galley);
499
500 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
501 if response.contains_pointer() {
502 let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos);
503
504 let dragged = false;
507 cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);
508 }
509 }
510
511 if let Some(mut cursor_range) = cursor_state.range(galley) {
512 let galley_rect = Rect::from_min_size(galley_pos, galley.size());
513 self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
514
515 if let Some(selection) = &self.selection {
516 if selection.primary.widget_id == response.id {
517 process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
518 }
519 }
520
521 if got_copy_event(ui.ctx()) {
522 self.copy_text(galley_pos, galley, &cursor_range);
523 }
524
525 cursor_state.set_range(Some(cursor_range));
526 }
527
528 let new_range = cursor_state.range(galley);
530 let selection_changed = old_range != new_range;
531
532 if let (true, Some(range)) = (selection_changed, new_range) {
533 if let Some(selection) = &mut self.selection {
537 let primary_changed = Some(range.primary) != old_range.map(|r| r.primary);
538 let secondary_changed = Some(range.secondary) != old_range.map(|r| r.secondary);
539
540 selection.layer_id = response.layer_id;
541
542 if primary_changed || !ui.style().interaction.multi_widget_text_select {
543 selection.primary =
544 WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley);
545 self.has_reached_primary = true;
546 }
547 if secondary_changed || !ui.style().interaction.multi_widget_text_select {
548 selection.secondary =
549 WidgetTextCursor::new(widget_id, range.secondary, galley_pos, galley);
550 self.has_reached_secondary = true;
551 }
552 } else {
553 self.selection = Some(CurrentSelection {
555 layer_id: response.layer_id,
556 primary: WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley),
557 secondary: WidgetTextCursor::new(
558 widget_id,
559 range.secondary,
560 galley_pos,
561 galley,
562 ),
563 });
564 self.has_reached_primary = true;
565 self.has_reached_secondary = true;
566 }
567 }
568
569 if let Some(range) = new_range {
571 let old_primary = old_selection.map(|s| s.primary);
572 let new_primary = self.selection.as_ref().map(|s| s.primary);
573 if let Some(new_primary) = new_primary {
574 let primary_changed = old_primary.map_or(true, |old| {
575 old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
576 });
577 if primary_changed && new_primary.widget_id == widget_id {
578 let is_fully_visible = ui.clip_rect().contains_rect(response.rect); if selection_changed && !is_fully_visible {
580 let row_height = estimate_row_height(galley);
582 let primary_cursor_rect =
583 cursor_rect(galley_pos, galley, &range.primary, row_height);
584 ui.scroll_to_rect(primary_cursor_rect, None);
585 }
586 }
587 }
588 }
589
590 let cursor_range = cursor_state.range(galley);
591
592 let mut new_vertex_indices = vec![];
593
594 if let Some(cursor_range) = cursor_range {
595 paint_text_selection(
596 galley,
597 ui.visuals(),
598 &cursor_range,
599 Some(&mut new_vertex_indices),
600 );
601 }
602
603 #[cfg(feature = "accesskit")]
604 super::accesskit_text::update_accesskit_for_text_widget(
605 ui.ctx(),
606 response.id,
607 cursor_range,
608 accesskit::Role::Label,
609 galley_pos,
610 galley,
611 );
612
613 new_vertex_indices
614 }
615}
616
617fn got_copy_event(ctx: &Context) -> bool {
618 ctx.input(|i| {
619 i.events
620 .iter()
621 .any(|e| matches!(e, Event::Copy | Event::Cut))
622 })
623}
624
625fn process_selection_key_events(
627 ctx: &Context,
628 galley: &Galley,
629 widget_id: Id,
630 cursor_range: &mut CursorRange,
631) -> bool {
632 let os = ctx.os();
633
634 let mut changed = false;
635
636 ctx.input(|i| {
637 for event in &i.events {
640 changed |= cursor_range.on_event(os, event, galley, widget_id);
641 }
642 });
643
644 changed
645}
646
647fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
648 let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley));
651
652 let copy_everything = cursor_range.is_empty() || everything_is_selected;
653
654 if copy_everything {
655 galley.text().to_owned()
656 } else {
657 cursor_range.slice_str(galley).to_owned()
658 }
659}
660
661fn estimate_row_height(galley: &Galley) -> f32 {
662 if let Some(row) = galley.rows.first() {
663 row.rect.height()
664 } else {
665 galley.size().y
666 }
667}