use std::sync::Arc;
use crate::{
layers::ShapeIdx, text::CCursor, text_selection::CCursorRange, Context, CursorIcon, Event,
Galley, Id, LayerId, Pos2, Rect, Response, Ui,
};
use super::{
text_cursor_state::cursor_rect,
visuals::{paint_text_selection, RowVertexIndices},
CursorRange, TextCursorState,
};
const DEBUG: bool = false; #[derive(Clone, Copy)]
struct WidgetTextCursor {
widget_id: Id,
ccursor: CCursor,
pos: Pos2,
}
impl WidgetTextCursor {
fn new(widget_id: Id, cursor: impl Into<CCursor>, galley_pos: Pos2, galley: &Galley) -> Self {
let ccursor = cursor.into();
let pos = pos_in_galley(galley_pos, galley, ccursor);
Self {
widget_id,
ccursor,
pos,
}
}
}
fn pos_in_galley(galley_pos: Pos2, galley: &Galley, ccursor: CCursor) -> Pos2 {
galley_pos + galley.pos_from_ccursor(ccursor).center().to_vec2()
}
impl std::fmt::Debug for WidgetTextCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WidgetTextCursor")
.field("widget_id", &self.widget_id.short_debug_format())
.field("ccursor", &self.ccursor.index)
.finish()
}
}
#[derive(Clone, Copy, Debug)]
struct CurrentSelection {
pub layer_id: LayerId,
pub primary: WidgetTextCursor,
pub secondary: WidgetTextCursor,
}
#[derive(Clone, Debug)]
pub struct LabelSelectionState {
selection: Option<CurrentSelection>,
selection_bbox_last_frame: Rect,
selection_bbox_this_frame: Rect,
any_hovered: bool,
is_dragging: bool,
has_reached_primary: bool,
has_reached_secondary: bool,
text_to_copy: String,
last_copied_galley_rect: Option<Rect>,
painted_selections: Vec<(ShapeIdx, Vec<RowVertexIndices>)>,
}
impl Default for LabelSelectionState {
fn default() -> Self {
Self {
selection: Default::default(),
selection_bbox_last_frame: Rect::NOTHING,
selection_bbox_this_frame: Rect::NOTHING,
any_hovered: Default::default(),
is_dragging: Default::default(),
has_reached_primary: Default::default(),
has_reached_secondary: Default::default(),
text_to_copy: Default::default(),
last_copied_galley_rect: Default::default(),
painted_selections: Default::default(),
}
}
}
impl LabelSelectionState {
pub(crate) fn register(ctx: &Context) {
ctx.on_begin_pass("LabelSelectionState", std::sync::Arc::new(Self::begin_pass));
ctx.on_end_pass("LabelSelectionState", std::sync::Arc::new(Self::end_pass));
}
pub fn load(ctx: &Context) -> Self {
let id = Id::new(ctx.viewport_id());
ctx.data(|data| data.get_temp::<Self>(id))
.unwrap_or_default()
}
pub fn store(self, ctx: &Context) {
let id = Id::new(ctx.viewport_id());
ctx.data_mut(|data| {
data.insert_temp(id, self);
});
}
fn begin_pass(ctx: &Context) {
let mut state = Self::load(ctx);
if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
}
state.selection_bbox_last_frame = state.selection_bbox_this_frame;
state.selection_bbox_this_frame = Rect::NOTHING;
state.any_hovered = false;
state.has_reached_primary = false;
state.has_reached_secondary = false;
state.text_to_copy.clear();
state.last_copied_galley_rect = None;
state.painted_selections.clear();
state.store(ctx);
}
fn end_pass(ctx: &Context) {
let mut state = Self::load(ctx);
if state.is_dragging {
ctx.set_cursor_icon(CursorIcon::Text);
}
if !state.has_reached_primary || !state.has_reached_secondary {
let prev_selection = state.selection.take();
if let Some(selection) = prev_selection {
ctx.graphics_mut(|layers| {
if let Some(list) = layers.get_mut(selection.layer_id) {
for (shape_idx, row_selections) in state.painted_selections.drain(..) {
list.mutate_shape(shape_idx, |shape| {
if let epaint::Shape::Text(text_shape) = &mut shape.shape {
let galley = Arc::make_mut(&mut text_shape.galley);
for row_selection in row_selections {
if let Some(row) = galley.rows.get_mut(row_selection.row) {
for vertex_index in row_selection.vertex_indices {
if let Some(vertex) = row
.visuals
.mesh
.vertices
.get_mut(vertex_index as usize)
{
vertex.color = epaint::Color32::TRANSPARENT;
}
}
}
}
}
});
}
}
});
}
}
let pressed_escape = ctx.input(|i| i.key_pressed(crate::Key::Escape));
let clicked_something_else = ctx.input(|i| i.pointer.any_pressed()) && !state.any_hovered;
let delected_everything = pressed_escape || clicked_something_else;
if delected_everything {
state.selection = None;
}
if ctx.input(|i| i.pointer.any_released()) {
state.is_dragging = false;
}
let text_to_copy = std::mem::take(&mut state.text_to_copy);
if !text_to_copy.is_empty() {
ctx.copy_text(text_to_copy);
}
state.store(ctx);
}
pub fn has_selection(&self) -> bool {
self.selection.is_some()
}
pub fn clear_selection(&mut self) {
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());
let new_text = selected_text(galley, cursor_range);
if new_text.is_empty() {
return;
}
if self.text_to_copy.is_empty() {
self.text_to_copy = new_text;
self.last_copied_galley_rect = Some(new_galley_rect);
return;
}
let Some(last_copied_galley_rect) = self.last_copied_galley_rect else {
self.text_to_copy = new_text;
self.last_copied_galley_rect = Some(new_galley_rect);
return;
};
if last_copied_galley_rect.bottom() <= new_galley_rect.top() {
self.text_to_copy.push('\n');
let vertical_distance = new_galley_rect.top() - last_copied_galley_rect.bottom();
if estimate_row_height(galley) * 0.5 < vertical_distance {
self.text_to_copy.push('\n');
}
} else {
let existing_ends_with_space =
self.text_to_copy.chars().last().map(|c| c.is_whitespace());
let new_text_starts_with_space_or_punctuation = new_text
.chars()
.next()
.map_or(false, |c| c.is_whitespace() || c.is_ascii_punctuation());
if existing_ends_with_space == Some(false) && !new_text_starts_with_space_or_punctuation
{
self.text_to_copy.push(' ');
}
}
self.text_to_copy.push_str(&new_text);
self.last_copied_galley_rect = Some(new_galley_rect);
}
pub fn label_text_selection(
ui: &Ui,
response: &Response,
galley_pos: Pos2,
mut galley: Arc<Galley>,
fallback_color: epaint::Color32,
underline: epaint::Stroke,
) {
let mut state = Self::load(ui.ctx());
let new_vertex_indices = state.on_label(ui, response, galley_pos, &mut galley);
let shape_idx = ui.painter().add(
epaint::TextShape::new(galley_pos, galley, fallback_color).with_underline(underline),
);
if !new_vertex_indices.is_empty() {
state
.painted_selections
.push((shape_idx, new_vertex_indices));
}
state.store(ui.ctx());
}
fn cursor_for(
&mut self,
ui: &Ui,
response: &Response,
galley_pos: Pos2,
galley: &Galley,
) -> TextCursorState {
let Some(selection) = &mut self.selection else {
return TextCursorState::default();
};
if selection.layer_id != response.layer_id {
return TextCursorState::default();
}
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 = galley_rect.intersect(ui.clip_rect());
let is_in_same_column = galley_rect
.x_range()
.intersects(self.selection_bbox_last_frame.x_range());
let has_reached_primary =
self.has_reached_primary || response.id == selection.primary.widget_id;
let has_reached_secondary =
self.has_reached_secondary || response.id == selection.secondary.widget_id;
let new_primary = if response.contains_pointer() {
Some(galley.cursor_from_pos(pointer_pos - galley_pos))
} else if is_in_same_column
&& !self.has_reached_primary
&& selection.primary.pos.y <= selection.secondary.pos.y
&& pointer_pos.y <= galley_rect.top()
&& galley_rect.top() <= selection.secondary.pos.y
{
if DEBUG {
ui.ctx()
.debug_text(format!("Upwards drag; include {:?}", response.id));
}
Some(galley.begin())
} else if is_in_same_column
&& has_reached_secondary
&& has_reached_primary
&& selection.secondary.pos.y <= selection.primary.pos.y
&& selection.secondary.pos.y <= galley_rect.bottom()
&& galley_rect.bottom() <= pointer_pos.y
{
if DEBUG {
ui.ctx()
.debug_text(format!("Downwards drag; include {:?}", response.id));
}
Some(galley.end())
} else {
None
};
if let Some(new_primary) = new_primary {
selection.primary =
WidgetTextCursor::new(response.id, new_primary, galley_pos, galley);
let drag_started = ui.input(|i| i.pointer.any_pressed());
if drag_started {
if selection.layer_id == response.layer_id {
if ui.input(|i| i.modifiers.shift) {
} else {
selection.secondary = selection.primary;
}
} else {
selection.layer_id = response.layer_id;
selection.secondary = selection.primary;
}
}
}
}
}
let has_primary = response.id == selection.primary.widget_id;
let has_secondary = response.id == selection.secondary.widget_id;
if has_primary {
selection.primary.pos = pos_in_galley(galley_pos, galley, selection.primary.ccursor);
}
if has_secondary {
selection.secondary.pos =
pos_in_galley(galley_pos, galley, selection.secondary.ccursor);
}
self.has_reached_primary |= has_primary;
self.has_reached_secondary |= has_secondary;
let primary = has_primary.then_some(selection.primary.ccursor);
let secondary = has_secondary.then_some(selection.secondary.ccursor);
match (primary, secondary) {
(Some(primary), Some(secondary)) => {
TextCursorState::from(CCursorRange { primary, secondary })
}
(Some(primary), None) => {
let secondary = if self.has_reached_secondary {
galley.begin().ccursor
} else {
galley.end().ccursor
};
TextCursorState::from(CCursorRange { primary, secondary })
}
(None, Some(secondary)) => {
let primary = if self.has_reached_primary {
galley.begin().ccursor
} else {
galley.end().ccursor
};
TextCursorState::from(CCursorRange { primary, secondary })
}
(None, None) => {
let is_in_middle = self.has_reached_primary != self.has_reached_secondary;
if is_in_middle {
if DEBUG {
response.ctx.debug_text(format!(
"widget in middle: {:?}, between {:?} and {:?}",
response.id, selection.primary.widget_id, selection.secondary.widget_id,
));
}
TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
} else {
TextCursorState::default()
}
}
}
}
fn on_label(
&mut self,
ui: &Ui,
response: &Response,
galley_pos: Pos2,
galley: &mut Arc<Galley>,
) -> Vec<RowVertexIndices> {
let widget_id = response.id;
if response.hovered {
ui.ctx().set_cursor_icon(CursorIcon::Text);
}
self.any_hovered |= response.hovered();
self.is_dragging |= response.is_pointer_button_down_on(); let old_selection = self.selection;
let mut cursor_state = self.cursor_for(ui, response, galley_pos, 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 dragged = false;
cursor_state.pointer_interaction(ui, response, cursor_at_pointer, galley, dragged);
}
}
if let Some(mut cursor_range) = cursor_state.range(galley) {
let galley_rect = Rect::from_min_size(galley_pos, galley.size());
self.selection_bbox_this_frame = self.selection_bbox_this_frame.union(galley_rect);
if let Some(selection) = &self.selection {
if selection.primary.widget_id == response.id {
process_selection_key_events(ui.ctx(), galley, response.id, &mut cursor_range);
}
}
if got_copy_event(ui.ctx()) {
self.copy_text(galley_pos, galley, &cursor_range);
}
cursor_state.set_range(Some(cursor_range));
}
let new_range = cursor_state.range(galley);
let selection_changed = old_range != new_range;
if let (true, Some(range)) = (selection_changed, new_range) {
if let Some(selection) = &mut self.selection {
let primary_changed = Some(range.primary) != old_range.map(|r| r.primary);
let secondary_changed = Some(range.secondary) != old_range.map(|r| r.secondary);
selection.layer_id = response.layer_id;
if primary_changed || !ui.style().interaction.multi_widget_text_select {
selection.primary =
WidgetTextCursor::new(widget_id, range.primary, galley_pos, 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);
self.has_reached_secondary = true;
}
} else {
self.selection = Some(CurrentSelection {
layer_id: response.layer_id,
primary: WidgetTextCursor::new(widget_id, range.primary, galley_pos, galley),
secondary: WidgetTextCursor::new(
widget_id,
range.secondary,
galley_pos,
galley,
),
});
self.has_reached_primary = true;
self.has_reached_secondary = true;
}
}
if let Some(range) = new_range {
let old_primary = old_selection.map(|s| s.primary);
let new_primary = self.selection.as_ref().map(|s| s.primary);
if let Some(new_primary) = new_primary {
let primary_changed = old_primary.map_or(true, |old| {
old.widget_id != new_primary.widget_id || old.ccursor != new_primary.ccursor
});
if primary_changed && new_primary.widget_id == widget_id {
let is_fully_visible = ui.clip_rect().contains_rect(response.rect); if selection_changed && !is_fully_visible {
let row_height = estimate_row_height(galley);
let primary_cursor_rect =
cursor_rect(galley_pos, galley, &range.primary, row_height);
ui.scroll_to_rect(primary_cursor_rect, None);
}
}
}
}
let cursor_range = cursor_state.range(galley);
let mut new_vertex_indices = vec![];
if let Some(cursor_range) = cursor_range {
paint_text_selection(
galley,
ui.visuals(),
&cursor_range,
Some(&mut new_vertex_indices),
);
}
#[cfg(feature = "accesskit")]
super::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
response.id,
cursor_range,
accesskit::Role::Label,
galley_pos,
galley,
);
new_vertex_indices
}
}
fn got_copy_event(ctx: &Context) -> bool {
ctx.input(|i| {
i.events
.iter()
.any(|e| matches!(e, Event::Copy | Event::Cut))
})
}
fn process_selection_key_events(
ctx: &Context,
galley: &Galley,
widget_id: Id,
cursor_range: &mut CursorRange,
) -> bool {
let os = ctx.os();
let mut changed = false;
ctx.input(|i| {
for event in &i.events {
changed |= cursor_range.on_event(os, event, galley, widget_id);
}
});
changed
}
fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
let everything_is_selected = cursor_range.contains(&CursorRange::select_all(galley));
let copy_everything = cursor_range.is_empty() || everything_is_selected;
if copy_everything {
galley.text().to_owned()
} else {
cursor_range.slice_str(galley).to_owned()
}
}
fn estimate_row_height(galley: &Galley) -> f32 {
if let Some(row) = galley.rows.first() {
row.rect.height()
} else {
galley.size().y
}
}