Cross-widget text select (#3870)
* Closes https://github.com/emilk/egui/issues/3816  Turn off with `style.interaction.multi_widget_text_select`. There is an API for this in `LabelSelectionState`, but it's pretty bare-bones. This became really hairy implementation-wise, but it works decently well. # Limitations * Drag-select to scroll doesn't work * A selection disappears if you scroll past one of its end-points * Only the text of labels and links are selectable ## TODO * [x] An option to turn it off * [x] An API for querying about the selected text, and to deselect it. * [x] Scrolling past selection behaves weird * [x] Shift-click to select a range
This commit is contained in:
parent
bcf032a08f
commit
41aad74552
|
|
@ -625,7 +625,7 @@ impl Context {
|
|||
/// ```
|
||||
pub fn begin_frame(&self, new_input: RawInput) {
|
||||
crate::profile_function!();
|
||||
|
||||
crate::text_selection::LabelSelectionState::begin_frame(self);
|
||||
self.write(|ctx| ctx.begin_frame_mut(new_input));
|
||||
}
|
||||
}
|
||||
|
|
@ -697,13 +697,13 @@ impl Context {
|
|||
|
||||
/// Read-write access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to.
|
||||
#[inline]
|
||||
pub(crate) fn graphics_mut<R>(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R {
|
||||
pub fn graphics_mut<R>(&self, writer: impl FnOnce(&mut GraphicLayers) -> R) -> R {
|
||||
self.write(move |ctx| writer(&mut ctx.viewport().graphics))
|
||||
}
|
||||
|
||||
/// Read-only access to [`GraphicLayers`], where painted [`crate::Shape`]s are written to.
|
||||
#[inline]
|
||||
pub(crate) fn graphics<R>(&self, reader: impl FnOnce(&GraphicLayers) -> R) -> R {
|
||||
pub fn graphics<R>(&self, reader: impl FnOnce(&GraphicLayers) -> R) -> R {
|
||||
self.write(move |ctx| reader(&ctx.viewport().graphics))
|
||||
}
|
||||
|
||||
|
|
@ -1616,6 +1616,8 @@ impl Context {
|
|||
crate::gui_zoom::zoom_with_keyboard(self);
|
||||
}
|
||||
|
||||
crate::text_selection::LabelSelectionState::end_frame(self);
|
||||
|
||||
let debug_texts = self.write(|ctx| std::mem::take(&mut ctx.debug_texts));
|
||||
if !debug_texts.is_empty() {
|
||||
// Show debug-text next to the cursor.
|
||||
|
|
@ -2041,7 +2043,7 @@ impl Context {
|
|||
/// Can be used to implement drag-and-drop (see relevant demo).
|
||||
pub fn translate_layer(&self, layer_id: LayerId, delta: Vec2) {
|
||||
if delta != Vec2::ZERO {
|
||||
self.graphics_mut(|g| g.list(layer_id).translate(delta));
|
||||
self.graphics_mut(|g| g.entry(layer_id).translate(delta));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2352,6 +2354,15 @@ impl Context {
|
|||
let font_image_size = self.fonts(|f| f.font_image_size());
|
||||
crate::introspection::font_texture_ui(ui, font_image_size);
|
||||
});
|
||||
|
||||
CollapsingHeader::new("Label text selection state")
|
||||
.default_open(false)
|
||||
.show(ui, |ui| {
|
||||
ui.label(format!(
|
||||
"{:#?}",
|
||||
crate::text_selection::LabelSelectionState::load(ui.ctx())
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
/// Show stats about the allocated textures.
|
||||
|
|
|
|||
|
|
@ -840,6 +840,7 @@ impl PointerState {
|
|||
}
|
||||
|
||||
/// Was any pointer button pressed (`!down -> down`) this frame?
|
||||
///
|
||||
/// This can sometimes return `true` even if `any_down() == false`
|
||||
/// because a press can be shorted than one frame.
|
||||
pub fn any_pressed(&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -110,7 +110,8 @@ impl LayerId {
|
|||
}
|
||||
|
||||
/// A unique identifier of a specific [`Shape`] in a [`PaintList`].
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ShapeIdx(pub usize);
|
||||
|
||||
/// A list of [`Shape`]s paired with a clip rectangle.
|
||||
|
|
@ -151,6 +152,12 @@ impl PaintList {
|
|||
self.0[idx.0] = ClippedShape { clip_rect, shape };
|
||||
}
|
||||
|
||||
/// Set the given shape to be empty (a `Shape::Noop`).
|
||||
#[inline(always)]
|
||||
pub fn reset_shape(&mut self, idx: ShapeIdx) {
|
||||
self.0[idx.0].shape = Shape::Noop;
|
||||
}
|
||||
|
||||
/// Translate each [`Shape`] and clip rectangle by this much, in-place
|
||||
pub fn translate(&mut self, delta: Vec2) {
|
||||
for ClippedShape { clip_rect, shape } in &mut self.0 {
|
||||
|
|
@ -165,20 +172,28 @@ impl PaintList {
|
|||
}
|
||||
}
|
||||
|
||||
/// This is where painted [`Shape`]s end up during a frame.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct GraphicLayers([IdMap<PaintList>; Order::COUNT]);
|
||||
pub struct GraphicLayers([IdMap<PaintList>; Order::COUNT]);
|
||||
|
||||
impl GraphicLayers {
|
||||
pub fn list(&mut self, layer_id: LayerId) -> &mut PaintList {
|
||||
/// Get or insert the [`PaintList`] for the given [`LayerId`].
|
||||
pub fn entry(&mut self, layer_id: LayerId) -> &mut PaintList {
|
||||
self.0[layer_id.order as usize]
|
||||
.entry(layer_id.id)
|
||||
.or_default()
|
||||
}
|
||||
|
||||
/// Get the [`PaintList`] for the given [`LayerId`].
|
||||
pub fn get(&self, layer_id: LayerId) -> Option<&PaintList> {
|
||||
self.0[layer_id.order as usize].get(&layer_id.id)
|
||||
}
|
||||
|
||||
/// Get the [`PaintList`] for the given [`LayerId`].
|
||||
pub fn get_mut(&mut self, layer_id: LayerId) -> Option<&mut PaintList> {
|
||||
self.0[layer_id.order as usize].get_mut(&layer_id.id)
|
||||
}
|
||||
|
||||
pub fn drain(&mut self, area_order: &[LayerId]) -> Vec<ClippedShape> {
|
||||
crate::profile_function!();
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ impl Painter {
|
|||
impl Painter {
|
||||
#[inline]
|
||||
fn paint_list<R>(&self, writer: impl FnOnce(&mut PaintList) -> R) -> R {
|
||||
self.ctx.graphics_mut(|g| writer(g.list(self.layer_id)))
|
||||
self.ctx.graphics_mut(|g| writer(g.entry(self.layer_id)))
|
||||
}
|
||||
|
||||
fn transform_shape(&self, shape: &mut Shape) {
|
||||
|
|
@ -257,21 +257,21 @@ impl Painter {
|
|||
/// # Paint different primitives
|
||||
impl Painter {
|
||||
/// Paints a line from the first point to the second.
|
||||
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) {
|
||||
pub fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<Stroke>) -> ShapeIdx {
|
||||
self.add(Shape::LineSegment {
|
||||
points,
|
||||
stroke: stroke.into(),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/// Paints a horizontal line.
|
||||
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) {
|
||||
self.add(Shape::hline(x, y, stroke));
|
||||
pub fn hline(&self, x: impl Into<Rangef>, y: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
|
||||
self.add(Shape::hline(x, y, stroke))
|
||||
}
|
||||
|
||||
/// Paints a vertical line.
|
||||
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) {
|
||||
self.add(Shape::vline(x, y, stroke));
|
||||
pub fn vline(&self, x: f32, y: impl Into<Rangef>, stroke: impl Into<Stroke>) -> ShapeIdx {
|
||||
self.add(Shape::vline(x, y, stroke))
|
||||
}
|
||||
|
||||
pub fn circle(
|
||||
|
|
@ -280,31 +280,36 @@ impl Painter {
|
|||
radius: f32,
|
||||
fill_color: impl Into<Color32>,
|
||||
stroke: impl Into<Stroke>,
|
||||
) {
|
||||
) -> ShapeIdx {
|
||||
self.add(CircleShape {
|
||||
center,
|
||||
radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: stroke.into(),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn circle_filled(&self, center: Pos2, radius: f32, fill_color: impl Into<Color32>) {
|
||||
pub fn circle_filled(
|
||||
&self,
|
||||
center: Pos2,
|
||||
radius: f32,
|
||||
fill_color: impl Into<Color32>,
|
||||
) -> ShapeIdx {
|
||||
self.add(CircleShape {
|
||||
center,
|
||||
radius,
|
||||
fill: fill_color.into(),
|
||||
stroke: Default::default(),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) {
|
||||
pub fn circle_stroke(&self, center: Pos2, radius: f32, stroke: impl Into<Stroke>) -> ShapeIdx {
|
||||
self.add(CircleShape {
|
||||
center,
|
||||
radius,
|
||||
fill: Default::default(),
|
||||
stroke: stroke.into(),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rect(
|
||||
|
|
@ -313,8 +318,8 @@ impl Painter {
|
|||
rounding: impl Into<Rounding>,
|
||||
fill_color: impl Into<Color32>,
|
||||
stroke: impl Into<Stroke>,
|
||||
) {
|
||||
self.add(RectShape::new(rect, rounding, fill_color, stroke));
|
||||
) -> ShapeIdx {
|
||||
self.add(RectShape::new(rect, rounding, fill_color, stroke))
|
||||
}
|
||||
|
||||
pub fn rect_filled(
|
||||
|
|
@ -322,8 +327,8 @@ impl Painter {
|
|||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
fill_color: impl Into<Color32>,
|
||||
) {
|
||||
self.add(RectShape::filled(rect, rounding, fill_color));
|
||||
) -> ShapeIdx {
|
||||
self.add(RectShape::filled(rect, rounding, fill_color))
|
||||
}
|
||||
|
||||
pub fn rect_stroke(
|
||||
|
|
@ -331,8 +336,8 @@ impl Painter {
|
|||
rect: Rect,
|
||||
rounding: impl Into<Rounding>,
|
||||
stroke: impl Into<Stroke>,
|
||||
) {
|
||||
self.add(RectShape::stroke(rect, rounding, stroke));
|
||||
) -> ShapeIdx {
|
||||
self.add(RectShape::stroke(rect, rounding, stroke))
|
||||
}
|
||||
|
||||
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
|
||||
|
|
@ -366,8 +371,14 @@ impl Painter {
|
|||
/// .paint_at(ui, rect);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn image(&self, texture_id: epaint::TextureId, rect: Rect, uv: Rect, tint: Color32) {
|
||||
self.add(Shape::image(texture_id, rect, uv, tint));
|
||||
pub fn image(
|
||||
&self,
|
||||
texture_id: epaint::TextureId,
|
||||
rect: Rect,
|
||||
uv: Rect,
|
||||
tint: Color32,
|
||||
) -> ShapeIdx {
|
||||
self.add(Shape::image(texture_id, rect, uv, tint))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -720,6 +720,12 @@ pub struct Interaction {
|
|||
|
||||
/// Can you select the text on a [`crate::Label`] by default?
|
||||
pub selectable_labels: bool,
|
||||
|
||||
/// Can the user select text that span multiple labels?
|
||||
///
|
||||
/// The default is `true`, but text seelction can be slightly glitchy,
|
||||
/// so you may want to disable it.
|
||||
pub multi_widget_text_select: bool,
|
||||
}
|
||||
|
||||
/// Controls the visual style (colors etc) of egui.
|
||||
|
|
@ -1120,6 +1126,7 @@ impl Default for Interaction {
|
|||
show_tooltips_only_when_still: true,
|
||||
tooltip_delay: 0.0,
|
||||
selectable_labels: true,
|
||||
multi_widget_text_select: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1580,6 +1587,7 @@ impl Interaction {
|
|||
show_tooltips_only_when_still,
|
||||
tooltip_delay,
|
||||
selectable_labels,
|
||||
multi_widget_text_select,
|
||||
} = self;
|
||||
ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side"));
|
||||
ui.add(
|
||||
|
|
@ -1590,7 +1598,13 @@ impl Interaction {
|
|||
"Only show tooltips if mouse is still",
|
||||
);
|
||||
ui.add(Slider::new(tooltip_delay, 0.0..=1.0).text("tooltip_delay"));
|
||||
ui.checkbox(selectable_labels, "Selectable text in labels");
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(selectable_labels, "Selectable text in labels");
|
||||
if *selectable_labels {
|
||||
ui.checkbox(multi_widget_text_select, "Across multiple labels");
|
||||
}
|
||||
});
|
||||
|
||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,10 +234,10 @@ impl CCursorRange {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn two(min: CCursor, max: CCursor) -> Self {
|
||||
pub fn two(min: impl Into<CCursor>, max: impl Into<CCursor>) -> Self {
|
||||
Self {
|
||||
primary: max,
|
||||
secondary: min,
|
||||
primary: max.into(),
|
||||
secondary: min.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,39 +1,23 @@
|
|||
use epaint::{Galley, Pos2};
|
||||
|
||||
use crate::{Context, CursorIcon, Event, Id, Response, Ui};
|
||||
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, 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));
|
||||
}
|
||||
/// Turn on to help debug this
|
||||
const DEBUG: bool = false; // TODO: don't merge this while `true`
|
||||
|
||||
fn paint_selection(
|
||||
ui: &Ui,
|
||||
_response: &Response,
|
||||
galley_pos: Pos2,
|
||||
galley: &Galley,
|
||||
cursor_state: &TextCursorState,
|
||||
painted_shape_idx: &mut Vec<ShapeIdx>,
|
||||
) {
|
||||
let cursor_range = cursor_state.range(galley);
|
||||
|
||||
if let Some(cursor_range) = cursor_range {
|
||||
|
|
@ -45,109 +29,593 @@ pub fn label_text_selection(ui: &Ui, response: &Response, galley_pos: Pos2, gall
|
|||
galley_pos,
|
||||
galley,
|
||||
&cursor_range,
|
||||
Some(painted_shape_idx),
|
||||
);
|
||||
|
||||
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,
|
||||
_response.id,
|
||||
cursor_range,
|
||||
accesskit::Role::StaticText,
|
||||
galley_pos,
|
||||
galley,
|
||||
);
|
||||
}
|
||||
|
||||
if !cursor_state.is_empty() {
|
||||
LabelSelectionState::store(ui.ctx(), response.id, cursor_state);
|
||||
/// One end of a text selection, inside any widget.
|
||||
#[derive(Clone, Copy)]
|
||||
struct WidgetTextCursor {
|
||||
widget_id: Id,
|
||||
ccursor: CCursor,
|
||||
|
||||
/// Last known screen position
|
||||
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 {
|
||||
/// The selection is in this layer.
|
||||
///
|
||||
/// This is to constrain a selection to a single Window.
|
||||
pub layer_id: LayerId,
|
||||
|
||||
/// When selecting with a mouse, this is where the mouse was released.
|
||||
/// When moving with e.g. shift+arrows, this is what moves.
|
||||
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||
pub primary: WidgetTextCursor,
|
||||
|
||||
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||
/// This part of the cursor does not move when shift is down.
|
||||
pub secondary: WidgetTextCursor,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LabelSelectionState {
|
||||
/// The current selection, if any.
|
||||
selection: TextCursorState,
|
||||
selection: Option<CurrentSelection>,
|
||||
|
||||
selection_bbox_last_frame: Rect,
|
||||
selection_bbox_this_frame: Rect,
|
||||
|
||||
/// Any label hovered this frame?
|
||||
any_hovered: bool,
|
||||
|
||||
/// Are we in drag-to-select state?
|
||||
is_dragging: bool,
|
||||
|
||||
/// Have we reached the widget containing the primary selection?
|
||||
has_reached_primary: bool,
|
||||
|
||||
/// Have we reached the widget containing the secondary selection?
|
||||
has_reached_secondary: bool,
|
||||
|
||||
/// Accumulated text to copy.
|
||||
text_to_copy: String,
|
||||
last_copied_galley_rect: Option<Rect>,
|
||||
|
||||
/// Painted selections this frame.
|
||||
painted_shape_idx: Vec<ShapeIdx>,
|
||||
}
|
||||
|
||||
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_shape_idx: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LabelSelectionState {
|
||||
/// Load the range of text of text that is selected for the given widget.
|
||||
fn load(ctx: &Context, id: Id) -> TextCursorState {
|
||||
pub fn load(ctx: &Context) -> Self {
|
||||
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) {
|
||||
pub fn store(self, ctx: &Context) {
|
||||
ctx.data_mut(|data| {
|
||||
data.insert_temp(
|
||||
Id::NULL,
|
||||
Self {
|
||||
id: Some(id),
|
||||
selection,
|
||||
},
|
||||
);
|
||||
data.insert_temp(Id::NULL, self);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn begin_frame(ctx: &Context) {
|
||||
let mut state = Self::load(ctx);
|
||||
|
||||
if ctx.input(|i| i.pointer.any_pressed() && !i.modifiers.shift) {
|
||||
// Maybe a new selection is about to begin, but the old one is over:
|
||||
// state.selection = None; // TODO: this makes sense, but doesn't work as expected.
|
||||
}
|
||||
|
||||
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_shape_idx.clear();
|
||||
|
||||
state.store(ctx);
|
||||
}
|
||||
|
||||
pub fn end_frame(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 {
|
||||
// We didn't see both cursors this frame,
|
||||
// maybe because they are outside the visible area (scrolling),
|
||||
// or one disappeared. In either case we will have horrible glitches, so let's just deselect.
|
||||
|
||||
let prev_selection = state.selection.take();
|
||||
if let Some(selection) = prev_selection {
|
||||
// This was the first frame of glitch, so hide the
|
||||
// glitching by removing all painted selections:
|
||||
ctx.graphics_mut(|layers| {
|
||||
if let Some(list) = layers.get_mut(selection.layer_id) {
|
||||
for shape_idx in state.painted_shape_idx.drain(..) {
|
||||
list.reset_shape(shape_idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// We need to append or prepend the new text to the already copied text.
|
||||
// We need to do so intelligently.
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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 state = Self::load(ui.ctx());
|
||||
state.on_label(ui, response, galley_pos, galley);
|
||||
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 {
|
||||
// Nothing selected.
|
||||
return TextCursorState::default();
|
||||
};
|
||||
|
||||
if selection.layer_id != response.layer_id {
|
||||
// Selection is in another layer
|
||||
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() {
|
||||
// Dragging into this widget - easy case:
|
||||
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
|
||||
{
|
||||
// The user is dragging the text selection upwards, above the first selected widget (this one):
|
||||
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
|
||||
{
|
||||
// The user is dragging the text selection downwards, below this widget.
|
||||
// We move the cursor to the end of this widget,
|
||||
// (and we may do the same for the next widget too).
|
||||
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);
|
||||
|
||||
if response.drag_started() {
|
||||
if selection.layer_id == response.layer_id {
|
||||
if ui.input(|i| i.modifiers.shift) {
|
||||
// A continuation of a previous selection?
|
||||
} else {
|
||||
// A new selection.
|
||||
selection.secondary = selection.primary;
|
||||
}
|
||||
} else {
|
||||
// A new selection.
|
||||
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);
|
||||
|
||||
// The following code assumes we will encounter both ends of the cursor
|
||||
// at some point (but in any order).
|
||||
// If we don't (e.g. because one endpoint is outside the visible scroll areas),
|
||||
// we will have annoying failure cases.
|
||||
|
||||
match (primary, secondary) {
|
||||
(Some(primary), Some(secondary)) => {
|
||||
// This is the only selected label.
|
||||
TextCursorState::from(CCursorRange { primary, secondary })
|
||||
}
|
||||
|
||||
(Some(primary), None) => {
|
||||
// This labels contains only the primary cursor.
|
||||
let secondary = if self.has_reached_secondary {
|
||||
// Secondary was before primary.
|
||||
// Select everything up to the cursor.
|
||||
// We assume normal left-to-right and top-down layout order here.
|
||||
galley.begin().ccursor
|
||||
} else {
|
||||
// Select everything from the cursor onward:
|
||||
galley.end().ccursor
|
||||
};
|
||||
TextCursorState::from(CCursorRange { primary, secondary })
|
||||
}
|
||||
|
||||
(None, Some(secondary)) => {
|
||||
// This labels contains only the secondary cursor
|
||||
let primary = if self.has_reached_primary {
|
||||
// Primary was before secondary.
|
||||
// Select everything up to the cursor.
|
||||
// We assume normal left-to-right and top-down layout order here.
|
||||
galley.begin().ccursor
|
||||
} else {
|
||||
// Select everything from the cursor onward:
|
||||
galley.end().ccursor
|
||||
};
|
||||
TextCursorState::from(CCursorRange { primary, secondary })
|
||||
}
|
||||
|
||||
(None, None) => {
|
||||
// This widget has neither the primary or secondary cursor.
|
||||
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,
|
||||
));
|
||||
}
|
||||
// …but it is between the two selection endpoints, and so is fully selected.
|
||||
TextCursorState::from(CCursorRange::two(galley.begin(), galley.end()))
|
||||
} else {
|
||||
// Outside the selected range
|
||||
TextCursorState::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_label(&mut self, ui: &Ui, response: &Response, galley_pos: Pos2, galley: &Galley) {
|
||||
let widget_id = response.id;
|
||||
|
||||
if response.hovered {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Text);
|
||||
}
|
||||
|
||||
self.any_hovered |= response.hovered();
|
||||
self.is_dragging |= response.dragged();
|
||||
|
||||
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);
|
||||
|
||||
// This is where we handle start-of-drag and double-click-to-select.
|
||||
// Actual drag-to-select happens elsewhere.
|
||||
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));
|
||||
}
|
||||
|
||||
// Look for changes due to keyboard and/or mouse interaction:
|
||||
let new_range = cursor_state.range(galley);
|
||||
let selection_changed = old_range != new_range;
|
||||
|
||||
if let (true, Some(range)) = (selection_changed, new_range) {
|
||||
// --------------
|
||||
// Store results:
|
||||
|
||||
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 {
|
||||
// Start of a new selection
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll containing ScrollArea on cursor change:
|
||||
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); // 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, &range.primary, row_height);
|
||||
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paint_selection(
|
||||
ui,
|
||||
response,
|
||||
galley_pos,
|
||||
galley,
|
||||
&cursor_state,
|
||||
&mut self.painted_shape_idx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn got_copy_event(ctx: &Context) -> bool {
|
||||
ctx.input(|i| {
|
||||
i.events
|
||||
.iter()
|
||||
.any(|e| matches!(e, Event::Copy | Event::Cut))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns true if the cursor changed
|
||||
fn process_selection_key_events(
|
||||
ctx: &Context,
|
||||
galley: &Galley,
|
||||
widget_id: Id,
|
||||
cursor_range: &mut CursorRange,
|
||||
) {
|
||||
let mut copy_text = None;
|
||||
) -> bool {
|
||||
let os = ctx.os();
|
||||
|
||||
let mut changed = false;
|
||||
|
||||
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(os, event, galley, widget_id);
|
||||
}
|
||||
}
|
||||
changed |= cursor_range.on_event(os, event, galley, widget_id);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(copy_text) = copy_text {
|
||||
ctx.copy_text(copy_text);
|
||||
changed
|
||||
}
|
||||
|
||||
fn selected_text(galley: &Galley, cursor_range: &CursorRange) -> String {
|
||||
// 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 {
|
||||
galley.text().to_owned()
|
||||
} else {
|
||||
cursor_range.slice_str(galley).to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ 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 label_text_selection::LabelSelectionState;
|
||||
pub use text_cursor_state::TextCursorState;
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ impl TextCursorState {
|
|||
response: &Response,
|
||||
cursor_at_pointer: Cursor,
|
||||
galley: &Galley,
|
||||
is_being_dragged: bool,
|
||||
) -> bool {
|
||||
let text = galley.text();
|
||||
|
||||
|
|
@ -120,6 +121,7 @@ impl TextCursorState {
|
|||
true
|
||||
} else if response.sense.drag {
|
||||
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||||
// The start of a drag (or a click).
|
||||
if ui.input(|i| i.modifiers.shift) {
|
||||
if let Some(mut cursor_range) = self.range(galley) {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
|
|
@ -131,8 +133,8 @@ impl TextCursorState {
|
|||
self.set_range(Some(CursorRange::one(cursor_at_pointer)));
|
||||
}
|
||||
true
|
||||
} else if ui.input(|i| i.pointer.any_down()) && response.is_pointer_button_down_on() {
|
||||
// drag to select text:
|
||||
} else if is_being_dragged {
|
||||
// Drag to select text:
|
||||
if let Some(mut cursor_range) = self.range(galley) {
|
||||
cursor_range.primary = cursor_at_pointer;
|
||||
self.set_range(Some(cursor_range));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
use self::layers::ShapeIdx;
|
||||
|
||||
use super::CursorRange;
|
||||
|
||||
pub fn paint_text_selection(
|
||||
|
|
@ -8,6 +10,7 @@ pub fn paint_text_selection(
|
|||
galley_pos: Pos2,
|
||||
galley: &Galley,
|
||||
cursor_range: &CursorRange,
|
||||
mut out_shaped_idx: Option<&mut Vec<ShapeIdx>>,
|
||||
) {
|
||||
if cursor_range.is_empty() {
|
||||
return;
|
||||
|
|
@ -40,7 +43,10 @@ pub fn paint_text_selection(
|
|||
galley_pos + vec2(left, row.min_y()),
|
||||
galley_pos + vec2(right, row.max_y()),
|
||||
);
|
||||
painter.rect_filled(rect, 0.0, color);
|
||||
let shape_idx = painter.rect_filled(rect, 0.0, color);
|
||||
if let Some(out_shaped_idx) = &mut out_shaped_idx {
|
||||
out_shaped_idx.push(shape_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
use self::text_selection::LabelSelectionState;
|
||||
|
||||
/// Clickable text, that looks like a hyperlink.
|
||||
///
|
||||
/// To link to a web page, use [`Hyperlink`], [`Ui::hyperlink`] or [`Ui::hyperlink_to`].
|
||||
|
|
@ -53,7 +55,7 @@ impl Widget for Link {
|
|||
|
||||
let selectable = ui.style().interaction.selectable_labels;
|
||||
if selectable {
|
||||
crate::text_selection::label_text_selection(ui, &response, galley_pos, &galley);
|
||||
LabelSelectionState::label_text_selection(ui, &response, galley_pos, &galley);
|
||||
}
|
||||
|
||||
if response.hovered() {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ use std::sync::Arc;
|
|||
|
||||
use crate::*;
|
||||
|
||||
use self::text_selection::LabelSelectionState;
|
||||
|
||||
/// Static text.
|
||||
///
|
||||
/// Usually it is more convenient to use [`Ui::label`].
|
||||
|
|
@ -257,7 +259,7 @@ impl Widget for Label {
|
|||
|
||||
let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels);
|
||||
if selectable {
|
||||
crate::text_selection::label_text_selection(ui, &response, galley_pos, &galley);
|
||||
LabelSelectionState::label_text_selection(ui, &response, galley_pos, &galley);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -549,10 +549,14 @@ impl<'t> TextEdit<'t> {
|
|||
paint_cursor(&painter, ui.visuals(), cursor_rect);
|
||||
}
|
||||
|
||||
let did_interact =
|
||||
state
|
||||
.cursor
|
||||
.pointer_interaction(ui, &response, cursor_at_pointer, &galley);
|
||||
let is_being_dragged = ui.ctx().memory(|m| m.is_being_dragged(response.id));
|
||||
let did_interact = state.cursor.pointer_interaction(
|
||||
ui,
|
||||
&response,
|
||||
cursor_at_pointer,
|
||||
&galley,
|
||||
is_being_dragged,
|
||||
);
|
||||
|
||||
if did_interact {
|
||||
ui.memory_mut(|mem| mem.request_focus(response.id));
|
||||
|
|
@ -661,6 +665,7 @@ impl<'t> TextEdit<'t> {
|
|||
galley_pos,
|
||||
&galley,
|
||||
&cursor_range,
|
||||
None,
|
||||
);
|
||||
|
||||
let primary_cursor_rect =
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ impl Default for Demos {
|
|||
Box::<super::MiscDemoWindow>::default(),
|
||||
Box::<super::multi_touch::MultiTouch>::default(),
|
||||
Box::<super::painting::Painting>::default(),
|
||||
Box::<super::panels::Panels>::default(),
|
||||
Box::<super::plot_demo::PlotDemo>::default(),
|
||||
Box::<super::scrolling::Scrolling>::default(),
|
||||
Box::<super::sliders::Sliders>::default(),
|
||||
|
|
@ -42,7 +43,6 @@ impl Default for Demos {
|
|||
Box::<super::widget_gallery::WidgetGallery>::default(),
|
||||
Box::<super::window_options::WindowOptions>::default(),
|
||||
Box::<super::tests::WindowResizeTest>::default(),
|
||||
Box::<super::window_with_panels::WindowWithPanels>::default(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub mod misc_demo_window;
|
|||
pub mod multi_touch;
|
||||
pub mod paint_bezier;
|
||||
pub mod painting;
|
||||
pub mod panels;
|
||||
pub mod password;
|
||||
pub mod plot_demo;
|
||||
pub mod scrolling;
|
||||
|
|
@ -31,7 +32,6 @@ pub mod text_layout;
|
|||
pub mod toggle_switch;
|
||||
pub mod widget_gallery;
|
||||
pub mod window_options;
|
||||
pub mod window_with_panels;
|
||||
|
||||
pub use {
|
||||
about::About, demo_app_windows::DemoWindows, misc_demo_window::MiscDemoWindow,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct WindowWithPanels {}
|
||||
pub struct Panels {}
|
||||
|
||||
impl super::Demo for WindowWithPanels {
|
||||
impl super::Demo for Panels {
|
||||
fn name(&self) -> &'static str {
|
||||
"🗖 Window With Panels"
|
||||
"🗖 Panels"
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
|
||||
use super::View as _;
|
||||
let window = egui::Window::new("Window with Panels")
|
||||
let window = egui::Window::new("Panels")
|
||||
.default_width(600.0)
|
||||
.default_height(400.0)
|
||||
.vscroll(false)
|
||||
|
|
@ -18,7 +18,7 @@ impl super::Demo for WindowWithPanels {
|
|||
}
|
||||
}
|
||||
|
||||
impl super::View for WindowWithPanels {
|
||||
impl super::View for Panels {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Note that the order we add the panels is very important!
|
||||
|
||||
|
|
@ -125,6 +125,21 @@ impl Rangef {
|
|||
max: self.max.min(other.max),
|
||||
}
|
||||
}
|
||||
|
||||
/// Do the two ranges intersect?
|
||||
///
|
||||
/// ```
|
||||
/// # use emath::Rangef;
|
||||
/// assert!(Rangef::new(0.0, 10.0).intersects(Rangef::new(5.0, 15.0)));
|
||||
/// assert!(Rangef::new(0.0, 10.0).intersects(Rangef::new(5.0, 6.0)));
|
||||
/// assert!(Rangef::new(0.0, 10.0).intersects(Rangef::new(10.0, 20.0)));
|
||||
/// assert!(!Rangef::new(0.0, 10.0).intersects(Rangef::new(20.0, 30.0)));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn intersects(self, other: Self) -> bool {
|
||||
other.min <= self.max && self.min <= other.max
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rangef> for RangeInclusive<f32> {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub struct CCursor {
|
|||
}
|
||||
|
||||
impl CCursor {
|
||||
#[inline]
|
||||
pub fn new(index: usize) -> Self {
|
||||
Self {
|
||||
index,
|
||||
|
|
@ -25,9 +26,17 @@ impl CCursor {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Cursor> for CCursor {
|
||||
#[inline]
|
||||
fn from(c: Cursor) -> Self {
|
||||
c.ccursor
|
||||
}
|
||||
}
|
||||
|
||||
/// Two `CCursor`s are considered equal if they refer to the same character boundary,
|
||||
/// even if one prefers the start of the next row.
|
||||
impl PartialEq for CCursor {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index
|
||||
}
|
||||
|
|
@ -106,6 +115,7 @@ pub struct PCursor {
|
|||
/// Two `PCursor`s are considered equal if they refer to the same character boundary,
|
||||
/// even if one prefers the start of the next row.
|
||||
impl PartialEq for PCursor {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.paragraph == other.paragraph && self.offset == other.offset
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,6 +426,9 @@ impl TextWrapping {
|
|||
/// from `egui::InputState` and can change at any time.
|
||||
/// - The atlas has become full. This can happen any time a new glyph is added
|
||||
/// to the atlas, which in turn can happen any time new text is laid out.
|
||||
///
|
||||
/// The name comes from typography, where a "galley" is a metal tray
|
||||
/// containing a column of set type, usually the size of a page of text.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct Galley {
|
||||
|
|
@ -672,6 +675,11 @@ impl Galley {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
|
||||
self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores
|
||||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
pub fn pos_from_pcursor(&self, pcursor: PCursor) -> Rect {
|
||||
let mut it = PCursor::default();
|
||||
|
|
@ -708,8 +716,13 @@ impl Galley {
|
|||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
pub fn pos_from_cursor(&self, cursor: &Cursor) -> Rect {
|
||||
self.pos_from_pcursor(cursor.pcursor) // pcursor is what TextEdit stores
|
||||
pub fn pos_from_ccursor(&self, ccursor: CCursor) -> Rect {
|
||||
self.pos_from_cursor(&self.from_ccursor(ccursor))
|
||||
}
|
||||
|
||||
/// Returns a 0-width Rect.
|
||||
pub fn pos_from_rcursor(&self, rcursor: RCursor) -> Rect {
|
||||
self.pos_from_cursor(&self.from_rcursor(rcursor))
|
||||
}
|
||||
|
||||
/// Cursor at the given position within the galley.
|
||||
|
|
|
|||
Loading…
Reference in New Issue