1use crate::{
2 layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
3 Galley, Id, LayerId, Pos2, Rect, Response, Ui,
4};
5
6use super::{
7 text_cursor_state::cursor_rect, visuals::paint_text_selection, CursorRange, TextCursorState,
8};
9
10const DEBUG: bool = false; fn paint_selection(
14 ui: &Ui,
15 _response: &Response,
16 galley_pos: Pos2,
17 galley: &Galley,
18 cursor_state: &TextCursorState,
19 painted_shape_idx: &mut Vec<ShapeIdx>,
20) {
21 let cursor_range = cursor_state.range(galley);
22
23 if let Some(cursor_range) = cursor_range {
24 paint_text_selection(
27 ui.painter(),
28 ui.visuals(),
29 galley_pos,
30 galley,
31 &cursor_range,
32 Some(painted_shape_idx),
33 );
34 }
35
36 #[cfg(feature = "accesskit")]
37 super::accesskit_text::update_accesskit_for_text_widget(
38 ui.ctx(),
39 _response.id,
40 cursor_range,
41 accesskit::Role::StaticText,
42 galley_pos,
43 galley,
44 );
45}
46
47#[derive(Clone, Copy)]
49struct WidgetTextCursor {
50 widget_id: Id,
51 ccursor: CCursor,
52
53 pos: Pos2,
55}
56
57impl WidgetTextCursor {
58 fn new(widget_id: Id, cursor: impl Into<CCursor>, galley_pos: Pos2, galley: &Galley) -> Self {
59 let ccursor = cursor.into();
60 let pos = pos_in_galley(galley_pos, galley, ccursor);
61 Self {
62 widget_id,
63 ccursor,
64 pos,
65 }
66 }
67}
68
69fn pos_in_galley(galley_pos: Pos2, galley: &Galley, ccursor: CCursor) -> Pos2 {
70 galley_pos + galley.pos_from_ccursor(ccursor).center().to_vec2()
71}
72
73impl std::fmt::Debug for WidgetTextCursor {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("WidgetTextCursor")
76 .field("widget_id", &self.widget_id.short_debug_format())
77 .field("ccursor", &self.ccursor.index)
78 .finish()
79 }
80}
81
82#[derive(Clone, Copy, Debug)]
83struct CurrentSelection {
84 pub layer_id: LayerId,
88
89 pub primary: WidgetTextCursor,
93
94 pub secondary: WidgetTextCursor,
97}
98
99#[derive(Clone, Debug)]
103pub struct LabelSelectionState {
104 selection: Option<CurrentSelection>,
106
107 selection_bbox_last_frame: Rect,
108 selection_bbox_this_frame: Rect,
109
110 any_hovered: bool,
112
113 is_dragging: bool,
115
116 has_reached_primary: bool,
118
119 has_reached_secondary: bool,
121
122 text_to_copy: String,
124 last_copied_galley_rect: Option<Rect>,
125
126 painted_shape_idx: Vec<ShapeIdx>,
128}
129
130impl Default for LabelSelectionState {
131 fn default() -> Self {
132 Self {
133 selection: Default::default(),
134 selection_bbox_last_frame: Rect::NOTHING,
135 selection_bbox_this_frame: Rect::NOTHING,
136 any_hovered: Default::default(),
137 is_dragging: Default::default(),
138 has_reached_primary: Default::default(),
139 has_reached_secondary: Default::default(),
140 text_to_copy: Default::default(),
141 last_copied_galley_rect: Default::default(),
142 painted_shape_idx: Default::default(),
143 }
144 }
145}
146
147impl LabelSelectionState {
148 pub(crate) fn register(ctx: &Context) {
149 ctx.on_begin_frame(
150 "LabelSelectionState",
151 std::sync::Arc::new(Self::begin_frame),
152 );
153 ctx.on_end_frame("LabelSelectionState", std::sync::Arc::new(Self::end_frame));
154 }
155
156 pub fn load(ctx: &Context) -> Self {
157 let id = Id::new(ctx.viewport_id());
158 ctx.data(|data| data.get_temp::<Self>(id))
159 .unwrap_or_default()
160 }
161
162 pub fn store(self, ctx: &Context) {
163 let id = Id::new(ctx.viewport_id());
164 ctx.data_mut(|data| {
165 data.insert_temp(id, self);
166 });
167 }
168
169 fn begin_frame(ctx: &Context) {
170 let mut state = Self::load(ctx);
171
172 if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
173 }
176
177 state.selection_bbox_last_frame = state.selection_bbox_this_frame;
178 state.selection_bbox_this_frame = Rect::NOTHING;
179
180 state.any_hovered = false;
181 state.has_reached_primary = false;
182 state.has_reached_secondary = false;
183 state.text_to_copy.clear();
184 state.last_copied_galley_rect = None;
185 state.painted_shape_idx.clear();
186
187 state.store(ctx);
188 }
189
190 fn end_frame(ctx: &Context) {
191 let mut state = Self::load(ctx);
192
193 if state.is_dragging {
194 ctx.set_cursor_icon(CursorIcon::Text);
195 }
196
197 if !state.has_reached_primary || !state.has_reached_secondary {
198 let prev_selection = state.selection.take();
203 if let Some(selection) = prev_selection {
204 ctx.graphics_mut(|layers| {
207 if let Some(list) = layers.get_mut(selection.layer_id) {
208 for shape_idx in state.painted_shape_idx.drain(..) {
209 list.reset_shape(shape_idx);
210 }
211 }
212 });
213 }
214 }
215
216 let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape));
217 let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !state.any_hovered;
218 let delected_everything = pressed_escape || clicked_something_else;
219
220 if delected_everything {
221 state.selection = None;
222 }
223
224 if ctx.input(|i| i.pointer.any_released()) {
225 state.is_dragging = false;
226 }
227
228 let text_to_copy = std::mem::take(&mut state.text_to_copy);
229 if !text_to_copy.is_empty() {
230 ctx.copy_text(text_to_copy);
231 }
232
233 state.store(ctx);
234 }
235
236 pub fn has_selection(&self) -> bool {
237 self.selection.is_some()
238 }
239
240 pub fn clear_selection(&mut self) {
241 self.selection = None;
242 }
243
244 fn copy_text(&mut self, galley_pos: Pos2, galley: &Galley, cursor_range: &CursorRange) {
245 let new_galley_rect = Rect::from_min_size(galley_pos, galley.size());
246 let new_text = selected_text(galley, cursor_range);
247 if new_text.is_empty() {
248 return;
249 }
250
251 if self.text_to_copy.is_empty() {
252 self.text_to_copy = new_text;
253 self.last_copied_galley_rect = Some(new_galley_rect);
254 return;
255 }
256
257 let Some(last_copied_galley_rect) = self.last_copied_galley_rect else {
258 self.text_to_copy = new_text;
259 self.last_copied_galley_rect = Some(new_galley_rect);
260 return;
261 };
262
263 if last_copied_galley_rect.bottom() <= new_galley_rect.top() {
267 self.text_to_copy.push('\n');
268 let vertical_distance = new_galley_rect.top() - last_copied_galley_rect.bottom();
269 if estimate_row_height(galley) * 0.5 < vertical_distance {
270 self.text_to_copy.push('\n');
271 }
272 } else {
273 let existing_ends_with_space =
274 self.text_to_copy.chars().last().map(|c| c.is_whitespace());
275
276 let new_text_starts_with_space_or_punctuation = new_text
277 .chars()
278 .next()
279 .map_or(false, |c| c.is_whitespace() || c.is_ascii_punctuation());
280
281 if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
282 {
283 self.text_to_copy.push(' ');
284 }
285 }
286
287 self.text_to_copy.push_str(&new_text);
288 self.last_copied_galley_rect = Some(new_galley_rect);
289 }
290
291 pub fn label_text_selection(ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) {
298 let mut state = Self::load(ui.ctx());
299 state.on_label(ui, response, galley_pos, galley);
300 state.store(ui.ctx());
301 }
302
303 fn cursor_for(
304 &mut self,
305 ui: &Ui,
306 response: &Response,
307 galley_pos: Pos2,
308 galley: &Galley,
309 ) -> TextCursorState {
310 let Some(selection) = &mut self.selection else {
311 return TextCursorState::default();
313 };
314
315 if selection.layer_id != response.layer_id {
316 return TextCursorState::default();
318 }
319
320 let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;
321
322 let may_select_widget =
323 multi_widget_text_select || selection.primary.widget_id == response.id;
324
325 if self.is_dragging && may_select_widget {
326 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
327 let galley_rect = Rect::from_min_size(galley_pos, galley.size());
328 let galley_rect = galley_rect.intersect(ui.clip_rect());
329
330 let is_in_same_column = galley_rect
331 .x_range()
332 .intersects(self.selection_bbox_last_frame.x_range());
333
334 let has_reached_primary =
335 self.has_reached_primary || response.id == selection.primary.widget_id;
336 let has_reached_secondary =
337 self.has_reached_secondary || response.id == selection.secondary.widget_id;
338
339 let new_primary = if response.contains_pointer() {
340 Some(galley.cursor_from_pos(pointer_pos - galley_pos))
342 } else if is_in_same_column
343 && !self.has_reached_primary
344 && selection.primary.pos.y <= selection.secondary.pos.y
345 && pointer_pos.y <= galley_rect.top()
346 && galley_rect.top() <= selection.secondary.pos.y
347 {
348 if DEBUG {
350 ui.ctx()
351 .debug_text(format!("Upwards drag; include {:?}", response.id));
352 }
353 Some(galley.begin())
354 } else if is_in_same_column
355 && has_reached_secondary
356 && has_reached_primary
357 && selection.secondary.pos.y <= selection.primary.pos.y
358 && selection.secondary.pos.y <= galley_rect.bottom()
359 && galley_rect.bottom() <= pointer_pos.y
360 {
361 if DEBUG {
365 ui.ctx()
366 .debug_text(format!("Downwards drag; include {:?}", response.id));
367 }
368 Some(galley.end())
369 } else {
370 None
371 };
372
373 if let Some(new_primary) = new_primary {
374 selection.primary =
375 WidgetTextCursor::new(response.id, new_primary, galley_pos, galley);
376
377 let drag_started = ui.input(|i| i.pointer.any_pressed());
379 if drag_started {
380 if selection.layer_id == response.layer_id {
381 if ui.input(|i| i.modifiers.shift) {
382 } else {
384 selection.secondary = selection.primary;
386 }
387 } else {
388 selection.layer_id = response.layer_id;
390 selection.secondary = selection.primary;
391 }
392 }
393 }
394 }
395 }
396
397 let has_primary = response.id == selection.primary.widget_id;
398 let has_secondary = response.id == selection.secondary.widget_id;
399
400 if has_primary {
401 selection.primary.pos = pos_in_galley(galley_pos, galley, selection.primary.ccursor);
402 }
403 if has_secondary {
404 selection.secondary.pos =
405 pos_in_galley(galley_pos, galley, selection.secondary.ccursor);
406 }
407
408 self.has_reached_primary |= has_primary;
409 self.has_reached_secondary |= has_secondary;
410
411 let primary = has_primary.then_some(selection.primary.ccursor);
412 let secondary = has_secondary.then_some(selection.secondary.ccursor);
413
414 match (primary, secondary) {
420 (Some(primary), Some(secondary)) => {
421 TextCursorState::from(CCursorRange { primary, secondary })
423 }
424
425 (Some(primary), None) => {
426 let secondary = if self.has_reached_secondary {
428 galley.begin().ccursor
432 } else {
433 galley.end().ccursor
435 };
436 TextCursorState::from(CCursorRange { primary, secondary })
437 }
438
439 (None, Some(secondary)) => {
440 let primary = if self.has_reached_primary {
442 galley.begin().ccursor
446 } else {
447 galley.end().ccursor
449 };
450 TextCursorState::from(CCursorRange { primary, secondary })
451 }
452
453 (None, None) => {
454 let is_in_middle = self.has_reached_primary != self.has_reached_secondary;
456 if is_in_middle {
457 if DEBUG {
458 response.ctx.debug_text(format!(
459 "widget in middle: {:?}, between {:?} and {:?}",
460 response.id, selection.primary.widget_id, selection.secondary.widget_id,
461 ));
462 }
463 TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
465 } else {
466 TextCursorState::default()
468 }
469 }
470 }
471 }
472
473 fn on_label(&mut self, ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) {
474 let widget_id = response.id;
475
476 if response.hovered {
477 ui.ctx().set_cursor_icon(CursorIcon::Text);
478 }
479
480 self.any_hovered |= response.hovered();
481 self.is_dragging |= response.is_pointer_button_down_on(); let old_selection = self.selection;
484
485 let mut cursor_state = self.cursor_for(ui, response, galley_pos, galley);
486
487 let old_range = cursor_state.range(galley);
488
489 if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
490 if response.contains_pointer() {
491 let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos);
492
493 let dragged = false;
496 cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);
497 }
498 }
499
500 if let Some(mut cursor_range) = cursor_state.range(galley) {
501 let galley_rect = Rect::from_min_size(galley_pos, galley.size());
502 self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
503
504 if let Some(selection) = &self.selection {
505 if selection.primary.widget_id == response.id {
506 process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
507 }
508 }
509
510 if got_copy_event(ui.ctx()) {
511 self.copy_text(galley_pos, galley, &cursor_range);
512 }
513
514 cursor_state.set_range(Some(cursor_range));
515 }
516
517 let new_range = cursor_state.range(galley);
519 let selection_changed = old_range != new_range;
520
521 if let (true, Some(range)) = (selection_changed, new_range) {
522 if let Some(selection) = &mut self.selection {
526 let primary_changed = Some(range.primary) != old_range.map(|r| r.primary);
527 let secondary_changed = Some(range.secondary) != old_range.map(|r| r.secondary);
528
529 selection.layer_id = response.layer_id;
530
531 if primary_changed || !ui.style().interaction.multi_widget_text_select {
532 selection.primary =
533 WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley);
534 self.has_reached_primary = true;
535 }
536 if secondary_changed || !ui.style().interaction.multi_widget_text_select {
537 selection.secondary =
538 WidgetTextCursor::new(widget_id, range.secondary, galley_pos, galley);
539 self.has_reached_secondary = true;
540 }
541 } else {
542 self.selection = Some(CurrentSelection {
544 layer_id: response.layer_id,
545 primary: WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley),
546 secondary: WidgetTextCursor::new(
547 widget_id,
548 range.secondary,
549 galley_pos,
550 galley,
551 ),
552 });
553 self.has_reached_primary = true;
554 self.has_reached_secondary = true;
555 }
556 }
557
558 if let Some(range) = new_range {
560 let old_primary = old_selection.map(|s| s.primary);
561 let new_primary = self.selection.as_ref().map(|s| s.primary);
562 if let Some(new_primary) = new_primary {
563 let primary_changed = old_primary.map_or(true, |old| {
564 old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
565 });
566 if primary_changed && new_primary.widget_id == widget_id {
567 let is_fully_visible = ui.clip_rect().contains_rect(response.rect); if selection_changed && !is_fully_visible {
569 let row_height = estimate_row_height(galley);
571 let primary_cursor_rect =
572 cursor_rect(galley_pos, galley, &range.primary, row_height);
573 ui.scroll_to_rect(primary_cursor_rect, None);
574 }
575 }
576 }
577 }
578
579 paint_selection(
580 ui,
581 response,
582 galley_pos,
583 galley,
584 &cursor_state,
585 &mut self.painted_shape_idx,
586 );
587 }
588}
589
590fn got_copy_event(ctx: &Context) -> bool {
591 ctx.input(|i| {
592 i.events
593 .iter()
594 .any(|e| matches!(e, Event::Copy | Event::Cut))
595 })
596}
597
598fn process_selection_key_events(
600 ctx: &Context,
601 galley: &Galley,
602 widget_id: Id,
603 cursor_range: &mut CursorRange,
604) -> bool {
605 let os = ctx.os();
606
607 let mut changed = false;
608
609 ctx.input(|i| {
610 for event in &i.events {
613 changed |= cursor_range.on_event(os, event, galley, widget_id);
614 }
615 });
616
617 changed
618}
619
620fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
621 let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley));
624
625 let copy_everything = cursor_range.is_empty() || everything_is_selected;
626
627 if copy_everything {
628 galley.text().to_owned()
629 } else {
630 cursor_range.slice_str(galley).to_owned()
631 }
632}
633
634fn estimate_row_height(galley: &Galley) -> f32 {
635 if let Some(row) = galley.rows.first() {
636 row.rect.height()
637 } else {
638 galley.size().y
639 }
640}