diff --git a/crates/egui/src/text_selection/accesskit_text.rs b/crates/egui/src/text_selection/accesskit_text.rs index d0c3869038d..dedbc79dc25 100644 --- a/crates/egui/src/text_selection/accesskit_text.rs +++ b/crates/egui/src/text_selection/accesskit_text.rs @@ -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}; @@ -8,7 +10,7 @@ pub fn update_accesskit_for_text_widget( widget_id: Id, cursor_range: Option, role: accesskit::Role, - galley_pos: Pos2, + global_from_galley: TSTransform, galley: &Galley, ) { let parent_id = ctx.accesskit_node_builder(widget_id, |builder| { @@ -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(), diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index ea5f3c9c63a..aa9f0986aa8 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -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, @@ -25,9 +27,14 @@ struct WidgetTextCursor { } impl WidgetTextCursor { - fn new(widget_id: Id, cursor: impl Into, galley_pos: Pos2, galley: &Galley) -> Self { + fn new( + widget_id: Id, + cursor: impl Into, + 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, @@ -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 { @@ -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; @@ -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 { @@ -321,6 +327,8 @@ 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 = @@ -328,7 +336,8 @@ impl LabelSelectionState { 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 @@ -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 @@ -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()); @@ -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; @@ -479,11 +489,21 @@ impl LabelSelectionState { &mut self, ui: &Ui, response: &Response, - galley_pos: Pos2, + galley_pos_in_layer: Pos2, galley: &mut Arc, ) -> Vec { 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); } @@ -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. @@ -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 { @@ -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)); @@ -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, ), }); @@ -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); } } @@ -606,7 +636,7 @@ impl LabelSelectionState { response.id, cursor_range, accesskit::Role::Label, - galley_pos, + global_from_galley, galley, ); diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 61407353a85..ebc618b2c4b 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -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}; @@ -335,14 +335,14 @@ pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &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 } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 2619035a4b9..81dd6a448a1 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use emath::Rect; +use emath::{Rect, TSTransform}; use epaint::text::{cursor::CCursor, Galley, LayoutJob}; use crate::{ @@ -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); } @@ -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: @@ -837,7 +838,7 @@ impl TextEdit<'_> { id, cursor_range, role, - galley_pos, + TSTransform::from_translation(galley_pos.to_vec2()), &galley, ); }