-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor: move text selection logic to own module (#3843)
- Loading branch information
Showing
15 changed files
with
497 additions
and
480 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...i/src/widgets/text_edit/accesskit_text.rs → ...egui/src/text_selection/accesskit_text.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
use epaint::{Galley, Pos2}; | ||
|
||
use crate::{Context, CursorIcon, Event, Id, Response, Ui}; | ||
|
||
use super::{ | ||
text_cursor_state::cursor_rect, visuals::paint_text_selection, CursorRange, TextCursorState, | ||
}; | ||
|
||
/// Handle text selection state for a label or similar widget. | ||
/// | ||
/// Make sure the widget senses clicks and drags. | ||
/// | ||
/// This should be called after painting the text, because this will also | ||
/// paint the text cursor/selection on top. | ||
pub fn label_text_selection(ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) { | ||
let mut cursor_state = LabelSelectionState::load(ui.ctx(), response.id); | ||
let original_cursor = cursor_state.range(galley); | ||
|
||
if response.hovered { | ||
ui.ctx().set_cursor_icon(CursorIcon::Text); | ||
} else if !cursor_state.is_empty() && ui.input(|i| i.pointer.any_pressed()) { | ||
// We clicked somewhere else - deselect this label. | ||
cursor_state = Default::default(); | ||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state); | ||
} | ||
|
||
if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { | ||
let cursor_at_pointer = galley.cursor_from_pos(pointer_pos - galley_pos); | ||
cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley); | ||
} | ||
|
||
if let Some(mut cursor_range) = cursor_state.range(galley) { | ||
process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range); | ||
cursor_state.set_range(Some(cursor_range)); | ||
} | ||
|
||
let cursor_range = cursor_state.range(galley); | ||
|
||
if let Some(cursor_range) = cursor_range { | ||
// We paint the cursor on top of the text, in case | ||
// the text galley has backgrounds (as e.g. `code` snippets in markup do). | ||
paint_text_selection( | ||
ui.painter(), | ||
ui.visuals(), | ||
galley_pos, | ||
galley, | ||
&cursor_range, | ||
); | ||
|
||
let selection_changed = original_cursor != Some(cursor_range); | ||
|
||
let is_fully_visible = ui.clip_rect().contains_rect(response.rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531 | ||
|
||
if selection_changed && !is_fully_visible { | ||
// Scroll to keep primary cursor in view: | ||
let row_height = estimate_row_height(galley); | ||
let primary_cursor_rect = | ||
cursor_rect(galley_pos, galley, &cursor_range.primary, row_height); | ||
ui.scroll_to_rect(primary_cursor_rect, None); | ||
} | ||
} | ||
|
||
#[cfg(feature = "accesskit")] | ||
super::accesskit_text::update_accesskit_for_text_widget( | ||
ui.ctx(), | ||
response.id, | ||
cursor_range, | ||
accesskit::Role::StaticText, | ||
galley_pos, | ||
galley, | ||
); | ||
|
||
if !cursor_state.is_empty() { | ||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state); | ||
} | ||
} | ||
|
||
/// Handles text selection in labels (NOT in [`crate::TextEdit`])s. | ||
/// | ||
/// One state for all labels, because we only support text selection in one label at a time. | ||
#[derive(Clone, Copy, Debug, Default)] | ||
struct LabelSelectionState { | ||
/// Id of the (only) label with a selection, if any | ||
id: Option<Id>, | ||
|
||
/// The current selection, if any. | ||
selection: TextCursorState, | ||
} | ||
|
||
impl LabelSelectionState { | ||
/// Load the range of text of text that is selected for the given widget. | ||
fn load(ctx: &Context, id: Id) -> TextCursorState { | ||
ctx.data(|data| data.get_temp::<Self>(Id::NULL)) | ||
.and_then(|state| (state.id == Some(id)).then_some(state.selection)) | ||
.unwrap_or_default() | ||
} | ||
|
||
/// Load the range of text of text that is selected for the given widget. | ||
fn store(ctx: &Context, id: Id, selection: TextCursorState) { | ||
ctx.data_mut(|data| { | ||
data.insert_temp( | ||
Id::NULL, | ||
Self { | ||
id: Some(id), | ||
selection, | ||
}, | ||
); | ||
}); | ||
} | ||
} | ||
|
||
fn process_selection_key_events( | ||
ctx: &Context, | ||
galley: &Galley, | ||
widget_id: Id, | ||
cursor_range: &mut CursorRange, | ||
) { | ||
let mut copy_text = None; | ||
|
||
ctx.input(|i| { | ||
// NOTE: we have a lock on ui/ctx here, | ||
// so be careful to not call into `ui` or `ctx` again. | ||
|
||
for event in &i.events { | ||
match event { | ||
Event::Copy | Event::Cut => { | ||
// This logic means we can select everything in an ellided label (including the `…`) | ||
// and still copy the entire un-ellided text! | ||
let everything_is_selected = | ||
cursor_range.contains(&CursorRange::select_all(galley)); | ||
|
||
let copy_everything = cursor_range.is_empty() || everything_is_selected; | ||
|
||
if copy_everything { | ||
copy_text = Some(galley.text().to_owned()); | ||
} else { | ||
copy_text = Some(cursor_range.slice_str(galley).to_owned()); | ||
} | ||
} | ||
|
||
event => { | ||
cursor_range.on_event(ctx.os(), event, galley, widget_id); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
if let Some(copy_text) = copy_text { | ||
ctx.copy_text(copy_text); | ||
} | ||
} | ||
|
||
fn estimate_row_height(galley: &Galley) -> f32 { | ||
if let Some(row) = galley.rows.first() { | ||
row.rect.height() | ||
} else { | ||
galley.size().y | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
//! Helpers regarding text selection for labels and text edit. | ||
#[cfg(feature = "accesskit")] | ||
pub mod accesskit_text; | ||
|
||
mod cursor_range; | ||
mod label_text_selection; | ||
pub mod text_cursor_state; | ||
pub mod visuals; | ||
|
||
pub use cursor_range::{CCursorRange, CursorRange, PCursorRange}; | ||
pub use label_text_selection::label_text_selection; | ||
pub use text_cursor_state::TextCursorState; |
63 changes: 62 additions & 1 deletion
63
...c/widgets/text_edit/cursor_interaction.rs → ...i/src/text_selection/text_cursor_state.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
use crate::*; | ||
|
||
use super::CursorRange; | ||
|
||
pub fn paint_text_selection( | ||
painter: &Painter, | ||
visuals: &Visuals, | ||
galley_pos: Pos2, | ||
galley: &Galley, | ||
cursor_range: &CursorRange, | ||
) { | ||
if cursor_range.is_empty() { | ||
return; | ||
} | ||
|
||
// We paint the cursor selection on top of the text, so make it transparent: | ||
let color = visuals.selection.bg_fill.linear_multiply(0.5); | ||
let [min, max] = cursor_range.sorted_cursors(); | ||
let min = min.rcursor; | ||
let max = max.rcursor; | ||
|
||
for ri in min.row..=max.row { | ||
let row = &galley.rows[ri]; | ||
let left = if ri == min.row { | ||
row.x_offset(min.column) | ||
} else { | ||
row.rect.left() | ||
}; | ||
let right = if ri == max.row { | ||
row.x_offset(max.column) | ||
} else { | ||
let newline_size = if row.ends_with_newline { | ||
row.height() / 2.0 // visualize that we select the newline | ||
} else { | ||
0.0 | ||
}; | ||
row.rect.right() + newline_size | ||
}; | ||
let rect = Rect::from_min_max( | ||
galley_pos + vec2(left, row.min_y()), | ||
galley_pos + vec2(right, row.max_y()), | ||
); | ||
painter.rect_filled(rect, 0.0, color); | ||
} | ||
} | ||
|
||
/// Paint one end of the selection, e.g. the primary cursor. | ||
pub fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) { | ||
let stroke = visuals.text_cursor; | ||
|
||
let top = cursor_rect.center_top(); | ||
let bottom = cursor_rect.center_bottom(); | ||
|
||
painter.line_segment([top, bottom], (stroke.width, stroke.color)); | ||
|
||
if false { | ||
// Roof/floor: | ||
let extrusion = 3.0; | ||
let width = 1.0; | ||
painter.line_segment( | ||
[top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)], | ||
(width, stroke.color), | ||
); | ||
painter.line_segment( | ||
[bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)], | ||
(width, stroke.color), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.