Skip to content

Commit

Permalink
Fix text selection when using layer transforms
Browse files Browse the repository at this point in the history
  • Loading branch information
emilk committed Jan 28, 2025
1 parent fde562d commit 1cd5132
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 41 deletions.
8 changes: 5 additions & 3 deletions crates/egui/src/text_selection/accesskit_text.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{Context, Galley, Id, Pos2};
use emath::TSTransform;

use crate::{Context, Galley, Id};

use super::{text_cursor_state::is_word_char, CursorRange};

Expand All @@ -8,7 +10,7 @@ pub fn update_accesskit_for_text_widget(
widget_id: Id,
cursor_range: Option<CursorRange>,
role: accesskit::Role,
galley_pos: Pos2,
global_from_galley: TSTransform,
galley: &Galley,
) {
let parent_id = ctx.accesskit_node_builder(widget_id, |builder| {
Expand Down Expand Up @@ -43,7 +45,7 @@ pub fn update_accesskit_for_text_widget(
let row_id = parent_id.with(row_index);
ctx.accesskit_node_builder(row_id, |builder| {
builder.set_role(accesskit::Role::TextRun);
let rect = row.rect.translate(galley_pos.to_vec2());
let rect = global_from_galley * row.rect;
builder.set_bounds(accesskit::Rect {
x0: rect.min.x.into(),
y0: rect.min.y.into(),
Expand Down
78 changes: 54 additions & 24 deletions crates/egui/src/text_selection/label_text_selection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::sync::Arc;

use emath::TSTransform;

use crate::{
layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
Galley, Id, LayerId, Pos2, Rect, Response, Ui,
Expand All @@ -25,9 +27,14 @@ struct WidgetTextCursor {
}

impl WidgetTextCursor {
fn new(widget_id: Id, cursor: impl Into<CCursor>, galley_pos: Pos2, galley: &Galley) -> Self {
fn new(
widget_id: Id,
cursor: impl Into<CCursor>,
global_from_galley: TSTransform,
galley: &Galley,
) -> Self {
let ccursor = cursor.into();
let pos = pos_in_galley(galley_pos, galley, ccursor);
let pos = global_from_galley * pos_in_galley(galley, ccursor);
Self {
widget_id,
ccursor,
Expand All @@ -36,8 +43,8 @@ impl WidgetTextCursor {
}
}

fn pos_in_galley(galley_pos: Pos2, galley: &Galley, ccursor: CCursor) -> Pos2 {
galley_pos + galley.pos_from_ccursor(ccursor).center().to_vec2()
fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
galley.pos_from_ccursor(ccursor).center()
}

impl std::fmt::Debug for WidgetTextCursor {
Expand Down Expand Up @@ -228,8 +235,7 @@ impl LabelSelectionState {
self.selection = None;
}

fn copy_text(&mut self, galley_pos: Pos2, galley: &Galley, cursor_range: &CursorRange) {
let new_galley_rect = Rect::from_min_size(galley_pos, galley.size());
fn copy_text(&mut self, new_galley_rect: Rect, galley: &Galley, cursor_range: &CursorRange) {
let new_text = selected_text(galley, cursor_range);
if new_text.is_empty() {
return;
Expand Down Expand Up @@ -308,7 +314,7 @@ impl LabelSelectionState {
&mut self,
ui: &Ui,
response: &Response,
galley_pos: Pos2,
global_from_galley: TSTransform,
galley: &Galley,
) -> TextCursorState {
let Some(selection) = &mut self.selection else {
Expand All @@ -321,14 +327,17 @@ impl LabelSelectionState {
return TextCursorState::default();
}

let galley_from_global = global_from_galley.inverse();

let multi_widget_text_select = ui.style().interaction.multi_widget_text_select;

let may_select_widget =
multi_widget_text_select || selection.primary.widget_id == response.id;

if self.is_dragging && may_select_widget {
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
let galley_rect = Rect::from_min_size(galley_pos, galley.size());
let galley_rect =
global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
let galley_rect = galley_rect.intersect(ui.clip_rect());

let is_in_same_column = galley_rect
Expand All @@ -342,7 +351,7 @@ impl LabelSelectionState {

let new_primary = if response.contains_pointer() {
// Dragging into this widget - easy case:
Some(galley.cursor_from_pos(pointer_pos - galley_pos))
Some(galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2()))
} else if is_in_same_column
&& !self.has_reached_primary
&& selection.primary.pos.y <= selection.secondary.pos.y
Expand Down Expand Up @@ -376,7 +385,7 @@ impl LabelSelectionState {

if let Some(new_primary) = new_primary {
selection.primary =
WidgetTextCursor::new(response.id, new_primary, galley_pos, galley);
WidgetTextCursor::new(response.id, new_primary, global_from_galley, galley);

// We don't want the latency of `drag_started`.
let drag_started = ui.input(|i| i.pointer.any_pressed());
Expand All @@ -402,11 +411,12 @@ impl LabelSelectionState {
let has_secondary = response.id == selection.secondary.widget_id;

if has_primary {
selection.primary.pos = pos_in_galley(galley_pos, galley, selection.primary.ccursor);
selection.primary.pos =
global_from_galley * pos_in_galley(galley, selection.primary.ccursor);
}
if has_secondary {
selection.secondary.pos =
pos_in_galley(galley_pos, galley, selection.secondary.ccursor);
global_from_galley * pos_in_galley(galley, selection.secondary.ccursor);
}

self.has_reached_primary |= has_primary;
Expand Down Expand Up @@ -479,11 +489,21 @@ impl LabelSelectionState {
&mut self,
ui: &Ui,
response: &Response,
galley_pos: Pos2,
galley_pos_in_layer: Pos2,
galley: &mut Arc<Galley>,
) -> Vec<RowVertexIndices> {
let widget_id = response.id;

let global_from_layer = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
let layer_from_galley = TSTransform::from_translation(galley_pos_in_layer.to_vec2());
let galley_from_layer = layer_from_galley.inverse();
let layer_from_global = global_from_layer.inverse();
let galley_from_global = galley_from_layer * layer_from_global;
let global_from_galley = global_from_layer * layer_from_galley;

if response.hovered() {
ui.ctx().set_cursor_icon(CursorIcon::Text);
}
Expand All @@ -493,13 +513,14 @@ impl LabelSelectionState {

let old_selection = self.selection;

let mut cursor_state = self.cursor_for(ui, response, galley_pos, galley);
let mut cursor_state = self.cursor_for(ui, response, global_from_galley, galley);

let old_range = cursor_state.range(galley);

if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() {
if response.contains_pointer() {
let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos);
let cursor_at_pointer =
galley.cursor_from_pos((galley_from_global * pointer_pos).to_vec2());

// This is where we handle start-of-drag and double-click-to-select.
// Actual drag-to-select happens elsewhere.
Expand All @@ -509,7 +530,7 @@ impl LabelSelectionState {
}

if let Some(mut cursor_range) = cursor_state.range(galley) {
let galley_rect = Rect::from_min_size(galley_pos, galley.size());
let galley_rect = global_from_galley * Rect::from_min_size(Pos2::ZERO, galley.size());
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);

if let Some(selection) = &self.selection {
Expand All @@ -519,7 +540,7 @@ impl LabelSelectionState {
}

if got_copy_event(ui.ctx()) {
self.copy_text(galley_pos, galley, &cursor_range);
self.copy_text(galley_rect, galley, &cursor_range);
}

cursor_state.set_range(Some(cursor_range));
Expand All @@ -541,23 +562,32 @@ impl LabelSelectionState {

if primary_changed || !ui.style().interaction.multi_widget_text_select {
selection.primary =
WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley);
WidgetTextCursor::new(widget_id, range.primary, global_from_galley, galley);
self.has_reached_primary = true;
}
if secondary_changed || !ui.style().interaction.multi_widget_text_select {
selection.secondary =
WidgetTextCursor::new(widget_id, range.secondary, galley_pos, galley);
selection.secondary = WidgetTextCursor::new(
widget_id,
range.secondary,
global_from_galley,
galley,
);
self.has_reached_secondary = true;
}
} else {
// Start of a new selection
self.selection = Some(CurrentSelection {
layer_id: response.layer_id,
primary: WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley),
primary: WidgetTextCursor::new(
widget_id,
range.primary,
global_from_galley,
galley,
),
secondary: WidgetTextCursor::new(
widget_id,
range.secondary,
galley_pos,
global_from_galley,
galley,
),
});
Expand All @@ -580,7 +610,7 @@ impl LabelSelectionState {
// Scroll to keep primary cursor in view:
let row_height = estimate_row_height(galley);
let primary_cursor_rect =
cursor_rect(galley_pos, galley, &range.primary, row_height);
global_from_galley * cursor_rect(galley, &range.primary, row_height);
ui.scroll_to_rect(primary_cursor_rect, None);
}
}
Expand All @@ -606,7 +636,7 @@ impl LabelSelectionState {
response.id,
cursor_range,
accesskit::Role::Label,
galley_pos,
global_from_galley,
galley,
);

Expand Down
18 changes: 9 additions & 9 deletions crates/egui/src/text_selection/text_cursor_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use epaint::text::{
Galley,
};

use crate::{epaint, NumExt, Pos2, Rect, Response, Ui};
use crate::{epaint, NumExt, Rect, Response, Ui};

use super::{CCursorRange, CursorRange};

Expand Down Expand Up @@ -335,14 +335,14 @@ pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
&s[start_byte..end_byte]
}

/// The thin rectangle of one end of the selection, e.g. the primary cursor.
pub fn cursor_rect(galley_pos: Pos2, galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect {
let mut cursor_pos = galley
.pos_from_cursor(cursor)
.translate(galley_pos.to_vec2());
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);
/// The thin rectangle of one end of the selection, e.g. the primary cursor, in local galley coordinates.
pub fn cursor_rect(galley: &Galley, cursor: &Cursor, row_height: f32) -> Rect {
let mut cursor_pos = galley.pos_from_cursor(cursor);

// Handle completely empty galleys
cursor_pos = cursor_pos.expand(1.5);
// slightly above/below row
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height);

cursor_pos = cursor_pos.expand(1.5); // slightly above/below row

cursor_pos
}
11 changes: 6 additions & 5 deletions crates/egui/src/widgets/text_edit/builder.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::sync::Arc;

use emath::Rect;
use emath::{Rect, TSTransform};
use epaint::text::{cursor::CCursor, Galley, LayoutJob};

use crate::{
Expand Down Expand Up @@ -587,8 +587,8 @@ impl TextEdit<'_> {
&& ui.input(|i| i.pointer.is_moving())
{
// text cursor preview:
let cursor_rect =
cursor_rect(rect.min, &galley, &cursor_at_pointer, row_height);
let cursor_rect = TSTransform::from_translation(rect.min.to_vec2())
* cursor_rect(&galley, &cursor_at_pointer, row_height);
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
}

Expand Down Expand Up @@ -738,7 +738,8 @@ impl TextEdit<'_> {
if has_focus {
if let Some(cursor_range) = state.cursor.range(&galley) {
let primary_cursor_rect =
cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height);
cursor_rect(&galley, &cursor_range.primary, row_height)
.translate(galley_pos.to_vec2());

if response.changed() || selection_changed {
// Scroll to keep primary cursor in view:
Expand Down Expand Up @@ -837,7 +838,7 @@ impl TextEdit<'_> {
id,
cursor_range,
role,
galley_pos,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
}
Expand Down

0 comments on commit 1cd5132

Please sign in to comment.