Selectable text in Labels (#3814)
* Closes https://github.com/emilk/egui/issues/3804 Add ability to select the text in labels with mouse-drag, double-click, and keyboard (once clicked). Hit Cmd+C to copy the text. If everything of a label with elided text is selected, the copy command will copy the full non-elided text. IME and accesskit _should_ work, but is untested. You can control wether or not text in labels is selected globally in `style.interaction.selectable_labels` or on a per-label basis in `Label::selectable`. The default is ON. This also cleans up the `TextEdit` code somewhat, fixing a couple smaller bugs along the way. This does _not_ implement selecting text across multiple widgets. Text selection is only supported within a single `Label`, `TextEdit`, `Link` or `Hyperlink`.  ## TODO * [x] Test
This commit is contained in:
parent
301c72ba22
commit
5d2e192927
|
|
@ -73,7 +73,7 @@ pub struct IMEOutput {
|
||||||
/// Where the [`crate::TextEdit`] is located on screen.
|
/// Where the [`crate::TextEdit`] is located on screen.
|
||||||
pub rect: crate::Rect,
|
pub rect: crate::Rect,
|
||||||
|
|
||||||
/// Where the cursor is.
|
/// Where the primary cursor is.
|
||||||
///
|
///
|
||||||
/// This is a very thin rectangle.
|
/// This is a very thin rectangle.
|
||||||
pub cursor_rect: crate::Rect,
|
pub cursor_rect: crate::Rect,
|
||||||
|
|
|
||||||
|
|
@ -717,6 +717,9 @@ pub struct Interaction {
|
||||||
|
|
||||||
/// Delay in seconds before showing tooltips after the mouse stops moving
|
/// Delay in seconds before showing tooltips after the mouse stops moving
|
||||||
pub tooltip_delay: f64,
|
pub tooltip_delay: f64,
|
||||||
|
|
||||||
|
/// Can you select the text on a [`crate::Label`] by default?
|
||||||
|
pub selectable_labels: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Controls the visual style (colors etc) of egui.
|
/// Controls the visual style (colors etc) of egui.
|
||||||
|
|
@ -846,6 +849,7 @@ impl Visuals {
|
||||||
&self.widgets.noninteractive
|
&self.widgets.noninteractive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-interactive text color.
|
||||||
pub fn text_color(&self) -> Color32 {
|
pub fn text_color(&self) -> Color32 {
|
||||||
self.override_text_color
|
self.override_text_color
|
||||||
.unwrap_or_else(|| self.widgets.noninteractive.text_color())
|
.unwrap_or_else(|| self.widgets.noninteractive.text_color())
|
||||||
|
|
@ -1114,6 +1118,7 @@ impl Default for Interaction {
|
||||||
resize_grab_radius_corner: 10.0,
|
resize_grab_radius_corner: 10.0,
|
||||||
show_tooltips_only_when_still: true,
|
show_tooltips_only_when_still: true,
|
||||||
tooltip_delay: 0.0,
|
tooltip_delay: 0.0,
|
||||||
|
selectable_labels: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1573,6 +1578,7 @@ impl Interaction {
|
||||||
resize_grab_radius_corner,
|
resize_grab_radius_corner,
|
||||||
show_tooltips_only_when_still,
|
show_tooltips_only_when_still,
|
||||||
tooltip_delay,
|
tooltip_delay,
|
||||||
|
selectable_labels,
|
||||||
} = self;
|
} = self;
|
||||||
ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side"));
|
ui.add(Slider::new(resize_grab_radius_side, 0.0..=20.0).text("resize_grab_radius_side"));
|
||||||
ui.add(
|
ui.add(
|
||||||
|
|
@ -1583,6 +1589,7 @@ impl Interaction {
|
||||||
"Only show tooltips if mouse is still",
|
"Only show tooltips if mouse is still",
|
||||||
);
|
);
|
||||||
ui.add(Slider::new(tooltip_delay, 0.0..=1.0).text("tooltip_delay"));
|
ui.add(Slider::new(tooltip_delay, 0.0..=1.0).text("tooltip_delay"));
|
||||||
|
ui.checkbox(selectable_labels, "Selectable text in labels");
|
||||||
|
|
||||||
ui.vertical_centered(|ui| reset_button(ui, self));
|
ui.vertical_centered(|ui| reset_button(ui, self));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -544,7 +544,7 @@ impl<'a> Widget for DragValue<'a> {
|
||||||
ui.data_mut(|data| data.remove::<String>(id));
|
ui.data_mut(|data| data.remove::<String>(id));
|
||||||
ui.memory_mut(|mem| mem.request_focus(id));
|
ui.memory_mut(|mem| mem.request_focus(id));
|
||||||
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
|
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
|
||||||
state.set_ccursor_range(Some(text::CCursorRange::two(
|
state.cursor.set_char_range(Some(text::CCursorRange::two(
|
||||||
text::CCursor::default(),
|
text::CCursor::default(),
|
||||||
text::CCursor::new(value_text.chars().count()),
|
text::CCursor::new(value_text.chars().count()),
|
||||||
)));
|
)));
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,9 @@ impl Widget for Link {
|
||||||
let Self { text } = self;
|
let Self { text } = self;
|
||||||
let label = Label::new(text).sense(Sense::click());
|
let label = Label::new(text).sense(Sense::click());
|
||||||
|
|
||||||
let (pos, galley, response) = label.layout_in_ui(ui);
|
let (galley_pos, galley, response) = label.layout_in_ui(ui);
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, galley.text()));
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Link, galley.text()));
|
||||||
|
|
||||||
if response.hovered() {
|
|
||||||
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
let color = ui.visuals().hyperlink_color;
|
let color = ui.visuals().hyperlink_color;
|
||||||
let visuals = ui.style().interact(&response);
|
let visuals = ui.style().interact(&response);
|
||||||
|
|
@ -51,8 +47,18 @@ impl Widget for Link {
|
||||||
Stroke::NONE
|
Stroke::NONE
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.painter()
|
ui.painter().add(
|
||||||
.add(epaint::TextShape::new(pos, galley, color).with_underline(underline));
|
epaint::TextShape::new(galley_pos, galley.clone(), color).with_underline(underline),
|
||||||
|
);
|
||||||
|
|
||||||
|
let selectable = ui.style().interaction.selectable_labels;
|
||||||
|
if selectable {
|
||||||
|
crate::widgets::label::text_selection(ui, &response, galley_pos, &galley);
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.hovered() {
|
||||||
|
ui.ctx().set_cursor_icon(CursorIcon::PointingHand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::*;
|
use crate::{
|
||||||
|
text_edit::{
|
||||||
|
cursor_interaction::cursor_rect, paint_cursor_selection, CursorRange, TextCursorState,
|
||||||
|
},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
|
||||||
/// Static text.
|
/// Static text.
|
||||||
///
|
///
|
||||||
|
|
@ -23,6 +28,7 @@ pub struct Label {
|
||||||
wrap: Option<bool>,
|
wrap: Option<bool>,
|
||||||
truncate: bool,
|
truncate: bool,
|
||||||
sense: Option<Sense>,
|
sense: Option<Sense>,
|
||||||
|
selectable: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Label {
|
impl Label {
|
||||||
|
|
@ -32,6 +38,7 @@ impl Label {
|
||||||
wrap: None,
|
wrap: None,
|
||||||
truncate: false,
|
truncate: false,
|
||||||
sense: None,
|
sense: None,
|
||||||
|
selectable: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,6 +80,15 @@ impl Label {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Can the user select the text with the mouse?
|
||||||
|
///
|
||||||
|
/// Overrides [`crate::style::Interaction::selectable_labels`].
|
||||||
|
#[inline]
|
||||||
|
pub fn selectable(mut self, selectable: bool) -> Self {
|
||||||
|
self.selectable = Some(selectable);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Make the label respond to clicks and/or drags.
|
/// Make the label respond to clicks and/or drags.
|
||||||
///
|
///
|
||||||
/// By default, a label is inert and does not respond to click or drags.
|
/// By default, a label is inert and does not respond to click or drags.
|
||||||
|
|
@ -97,14 +113,35 @@ impl Label {
|
||||||
impl Label {
|
impl Label {
|
||||||
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
/// Do layout and position the galley in the ui, without painting it or adding widget info.
|
||||||
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, Arc<Galley>, Response) {
|
pub fn layout_in_ui(self, ui: &mut Ui) -> (Pos2, Arc<Galley>, Response) {
|
||||||
let sense = self.sense.unwrap_or_else(|| {
|
let selectable = self
|
||||||
// We only want to focus labels if the screen reader is on.
|
.selectable
|
||||||
|
.unwrap_or_else(|| ui.style().interaction.selectable_labels);
|
||||||
|
|
||||||
|
let mut sense = self.sense.unwrap_or_else(|| {
|
||||||
if ui.memory(|mem| mem.options.screen_reader) {
|
if ui.memory(|mem| mem.options.screen_reader) {
|
||||||
|
// We only want to focus labels if the screen reader is on.
|
||||||
Sense::focusable_noninteractive()
|
Sense::focusable_noninteractive()
|
||||||
} else {
|
} else {
|
||||||
Sense::hover()
|
Sense::hover()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if selectable {
|
||||||
|
// On touch screens (e.g. mobile in `eframe` web), should
|
||||||
|
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
|
||||||
|
// Since currently copying selected text in not supported on `eframe` web,
|
||||||
|
// we prioritize touch-scrolling:
|
||||||
|
let allow_drag_to_select = ui.input(|i| !i.any_touches());
|
||||||
|
|
||||||
|
let select_sense = if allow_drag_to_select {
|
||||||
|
Sense::click_and_drag()
|
||||||
|
} else {
|
||||||
|
Sense::click()
|
||||||
|
};
|
||||||
|
|
||||||
|
sense = sense.union(select_sense);
|
||||||
|
}
|
||||||
|
|
||||||
if let WidgetText::Galley(galley) = self.text {
|
if let WidgetText::Galley(galley) = self.text {
|
||||||
// If the user said "use this specific galley", then just use it:
|
// If the user said "use this specific galley", then just use it:
|
||||||
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
||||||
|
|
@ -178,28 +215,39 @@ impl Label {
|
||||||
|
|
||||||
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
|
let galley = ui.fonts(|fonts| fonts.layout_job(layout_job));
|
||||||
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
let (rect, response) = ui.allocate_exact_size(galley.size(), sense);
|
||||||
let pos = match galley.job.halign {
|
let galley_pos = match galley.job.halign {
|
||||||
Align::LEFT => rect.left_top(),
|
Align::LEFT => rect.left_top(),
|
||||||
Align::Center => rect.center_top(),
|
Align::Center => rect.center_top(),
|
||||||
Align::RIGHT => rect.right_top(),
|
Align::RIGHT => rect.right_top(),
|
||||||
};
|
};
|
||||||
(pos, galley, response)
|
(galley_pos, galley, response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Label {
|
impl Widget for Label {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
let (pos, galley, mut response) = self.layout_in_ui(ui);
|
// Interactive = the uses asked to sense interaction.
|
||||||
|
// We DON'T want to have the color respond just because the text is selectable;
|
||||||
|
// the cursor is enough to communicate that.
|
||||||
|
let interactive = self.sense.map_or(false, |sense| sense != Sense::hover());
|
||||||
|
|
||||||
|
let selectable = self.selectable;
|
||||||
|
|
||||||
|
let (galley_pos, galley, mut response) = self.layout_in_ui(ui);
|
||||||
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, galley.text()));
|
response.widget_info(|| WidgetInfo::labeled(WidgetType::Label, galley.text()));
|
||||||
|
|
||||||
if galley.elided {
|
|
||||||
// Show the full (non-elided) text on hover:
|
|
||||||
response = response.on_hover_text(galley.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
let response_color = ui.style().interact(&response).text_color();
|
if galley.elided {
|
||||||
|
// Show the full (non-elided) text on hover:
|
||||||
|
response = response.on_hover_text(galley.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_color = if interactive {
|
||||||
|
ui.style().interact(&response).text_color()
|
||||||
|
} else {
|
||||||
|
ui.style().visuals.text_color()
|
||||||
|
};
|
||||||
|
|
||||||
let underline = if response.has_focus() || response.highlighted() {
|
let underline = if response.has_focus() || response.highlighted() {
|
||||||
Stroke::new(1.0, response_color)
|
Stroke::new(1.0, response_color)
|
||||||
|
|
@ -207,10 +255,167 @@ impl Widget for Label {
|
||||||
Stroke::NONE
|
Stroke::NONE
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.painter()
|
ui.painter().add(
|
||||||
.add(epaint::TextShape::new(pos, galley, response_color).with_underline(underline));
|
epaint::TextShape::new(galley_pos, galley.clone(), response_color)
|
||||||
|
.with_underline(underline),
|
||||||
|
);
|
||||||
|
|
||||||
|
let selectable = selectable.unwrap_or_else(|| ui.style().interaction.selectable_labels);
|
||||||
|
if selectable {
|
||||||
|
text_selection(ui, &response, galley_pos, &galley);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle text selection state for a label or similar widget.
|
||||||
|
///
|
||||||
|
/// Make sure the widget senses to clicks and drags.
|
||||||
|
///
|
||||||
|
/// This should be called after painting the text, because this will also
|
||||||
|
/// paint the text cursor/selection on top.
|
||||||
|
pub fn 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, 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_cursor_selection(
|
||||||
|
ui.visuals(),
|
||||||
|
ui.painter(),
|
||||||
|
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")]
|
||||||
|
text_edit::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn estimate_row_height(galley: &Galley) -> f32 {
|
||||||
|
if let Some(row) = galley.rows.first() {
|
||||||
|
row.rect.height()
|
||||||
|
} else {
|
||||||
|
galley.size().y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_selection_key_events(
|
||||||
|
ui: &Ui,
|
||||||
|
galley: &Galley,
|
||||||
|
widget_id: Id,
|
||||||
|
cursor_range: &mut CursorRange,
|
||||||
|
) {
|
||||||
|
let mut copy_text = None;
|
||||||
|
|
||||||
|
ui.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(event, galley, widget_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(copy_text) = copy_text {
|
||||||
|
ui.ctx().copy_text(copy_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn store(ctx: &Context, id: Id, selection: TextCursorState) {
|
||||||
|
ctx.data_mut(|data| {
|
||||||
|
data.insert_temp(
|
||||||
|
Id::NULL,
|
||||||
|
Self {
|
||||||
|
id: Some(id),
|
||||||
|
selection,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
use crate::{Context, Galley, Id, Pos2};
|
||||||
|
|
||||||
|
use super::{cursor_interaction::is_word_char, CursorRange};
|
||||||
|
|
||||||
|
/// Update accesskit with the current text state.
|
||||||
|
pub fn update_accesskit_for_text_widget(
|
||||||
|
ctx: &Context,
|
||||||
|
widget_id: Id,
|
||||||
|
cursor_range: Option<CursorRange>,
|
||||||
|
role: accesskit::Role,
|
||||||
|
galley_pos: Pos2,
|
||||||
|
galley: &Galley,
|
||||||
|
) {
|
||||||
|
let parent_id = ctx.accesskit_node_builder(widget_id, |builder| {
|
||||||
|
let parent_id = widget_id;
|
||||||
|
|
||||||
|
if let Some(cursor_range) = &cursor_range {
|
||||||
|
let anchor = &cursor_range.secondary.rcursor;
|
||||||
|
let focus = &cursor_range.primary.rcursor;
|
||||||
|
builder.set_text_selection(accesskit::TextSelection {
|
||||||
|
anchor: accesskit::TextPosition {
|
||||||
|
node: parent_id.with(anchor.row).accesskit_id(),
|
||||||
|
character_index: anchor.column,
|
||||||
|
},
|
||||||
|
focus: accesskit::TextPosition {
|
||||||
|
node: parent_id.with(focus.row).accesskit_id(),
|
||||||
|
character_index: focus.column,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus);
|
||||||
|
|
||||||
|
builder.set_role(role);
|
||||||
|
|
||||||
|
parent_id
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(parent_id) = parent_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.with_accessibility_parent(parent_id, || {
|
||||||
|
for (row_index, row) in galley.rows.iter().enumerate() {
|
||||||
|
let row_id = parent_id.with(row_index);
|
||||||
|
ctx.accesskit_node_builder(row_id, |builder| {
|
||||||
|
builder.set_role(accesskit::Role::InlineTextBox);
|
||||||
|
let rect = row.rect.translate(galley_pos.to_vec2());
|
||||||
|
builder.set_bounds(accesskit::Rect {
|
||||||
|
x0: rect.min.x.into(),
|
||||||
|
y0: rect.min.y.into(),
|
||||||
|
x1: rect.max.x.into(),
|
||||||
|
y1: rect.max.y.into(),
|
||||||
|
});
|
||||||
|
builder.set_text_direction(accesskit::TextDirection::LeftToRight);
|
||||||
|
// TODO(mwcampbell): Set more node fields for the row
|
||||||
|
// once AccessKit adapters expose text formatting info.
|
||||||
|
|
||||||
|
let glyph_count = row.glyphs.len();
|
||||||
|
let mut value = String::new();
|
||||||
|
value.reserve(glyph_count);
|
||||||
|
let mut character_lengths = Vec::<u8>::with_capacity(glyph_count);
|
||||||
|
let mut character_positions = Vec::<f32>::with_capacity(glyph_count);
|
||||||
|
let mut character_widths = Vec::<f32>::with_capacity(glyph_count);
|
||||||
|
let mut word_lengths = Vec::<u8>::new();
|
||||||
|
let mut was_at_word_end = false;
|
||||||
|
let mut last_word_start = 0usize;
|
||||||
|
|
||||||
|
for glyph in &row.glyphs {
|
||||||
|
let is_word_char = is_word_char(glyph.chr);
|
||||||
|
if is_word_char && was_at_word_end {
|
||||||
|
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
||||||
|
last_word_start = character_lengths.len();
|
||||||
|
}
|
||||||
|
was_at_word_end = !is_word_char;
|
||||||
|
let old_len = value.len();
|
||||||
|
value.push(glyph.chr);
|
||||||
|
character_lengths.push((value.len() - old_len) as _);
|
||||||
|
character_positions.push(glyph.pos.x - row.rect.min.x);
|
||||||
|
character_widths.push(glyph.size.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.ends_with_newline {
|
||||||
|
value.push('\n');
|
||||||
|
character_lengths.push(1);
|
||||||
|
character_positions.push(row.rect.max.x - row.rect.min.x);
|
||||||
|
character_widths.push(0.0);
|
||||||
|
}
|
||||||
|
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
||||||
|
|
||||||
|
builder.set_value(value);
|
||||||
|
builder.set_character_lengths(character_lengths);
|
||||||
|
builder.set_character_positions(character_positions);
|
||||||
|
builder.set_character_widths(character_widths);
|
||||||
|
builder.set_word_lengths(word_lengths);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
use accesskit::Role;
|
|
||||||
use epaint::text::{cursor::*, Galley, LayoutJob};
|
use epaint::text::{cursor::*, Galley, LayoutJob};
|
||||||
|
|
||||||
use crate::{output::OutputEvent, *};
|
use crate::{output::OutputEvent, text_edit::cursor_interaction::cursor_rect, *};
|
||||||
|
|
||||||
use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState};
|
use super::{
|
||||||
|
cursor_interaction::{ccursor_next_word, ccursor_previous_word, find_line_start},
|
||||||
|
CCursorRange, CursorRange, TextEditOutput, TextEditState,
|
||||||
|
};
|
||||||
|
|
||||||
/// A text region that the user can edit the contents of.
|
/// A text region that the user can edit the contents of.
|
||||||
///
|
///
|
||||||
|
|
@ -173,7 +174,7 @@ impl<'t> TextEdit<'t> {
|
||||||
/// text_color,
|
/// text_color,
|
||||||
/// f32::INFINITY
|
/// f32::INFINITY
|
||||||
/// );
|
/// );
|
||||||
/// painter.galley(output.text_draw_pos, galley, text_color);
|
/// painter.galley(output.galley_pos, galley, text_color);
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
@ -527,6 +528,7 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||||
|
|
||||||
let singleline_offset = vec2(state.singleline_offset, 0.0);
|
let singleline_offset = vec2(state.singleline_offset, 0.0);
|
||||||
let cursor_at_pointer =
|
let cursor_at_pointer =
|
||||||
galley.cursor_from_pos(pointer_pos - response.rect.min + singleline_offset);
|
galley.cursor_from_pos(pointer_pos - response.rect.min + singleline_offset);
|
||||||
|
|
@ -536,54 +538,18 @@ impl<'t> TextEdit<'t> {
|
||||||
&& ui.input(|i| i.pointer.is_moving())
|
&& ui.input(|i| i.pointer.is_moving())
|
||||||
{
|
{
|
||||||
// preview:
|
// preview:
|
||||||
paint_cursor_end(
|
let cursor_rect =
|
||||||
ui,
|
cursor_rect(response.rect.min, &galley, &cursor_at_pointer, row_height);
|
||||||
row_height,
|
paint_cursor(&painter, ui.visuals(), cursor_rect);
|
||||||
&painter,
|
|
||||||
response.rect.min,
|
|
||||||
&galley,
|
|
||||||
&cursor_at_pointer,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.double_clicked() {
|
let did_interact =
|
||||||
// Select word:
|
state
|
||||||
let center = cursor_at_pointer;
|
.cursor
|
||||||
let ccursor_range = select_word_at(text.as_str(), center.ccursor);
|
.pointer_interaction(ui, &response, cursor_at_pointer, &galley);
|
||||||
state.set_cursor_range(Some(CursorRange {
|
|
||||||
primary: galley.from_ccursor(ccursor_range.primary),
|
if did_interact {
|
||||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
ui.memory_mut(|mem| mem.request_focus(response.id));
|
||||||
}));
|
|
||||||
} else if response.triple_clicked() {
|
|
||||||
// Select line:
|
|
||||||
let center = cursor_at_pointer;
|
|
||||||
let ccursor_range = select_line_at(text.as_str(), center.ccursor);
|
|
||||||
state.set_cursor_range(Some(CursorRange {
|
|
||||||
primary: galley.from_ccursor(ccursor_range.primary),
|
|
||||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
|
||||||
}));
|
|
||||||
} else if allow_drag_to_select {
|
|
||||||
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
|
||||||
ui.memory_mut(|mem| mem.request_focus(id));
|
|
||||||
if ui.input(|i| i.modifiers.shift) {
|
|
||||||
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
|
||||||
cursor_range.primary = cursor_at_pointer;
|
|
||||||
state.set_cursor_range(Some(cursor_range));
|
|
||||||
} else {
|
|
||||||
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.set_cursor_range(Some(CursorRange::one(cursor_at_pointer)));
|
|
||||||
}
|
|
||||||
} else if ui.input(|i| i.pointer.any_down())
|
|
||||||
&& response.is_pointer_button_down_on()
|
|
||||||
{
|
|
||||||
// drag to select text:
|
|
||||||
if let Some(mut cursor_range) = state.cursor_range(&galley) {
|
|
||||||
cursor_range.primary = cursor_at_pointer;
|
|
||||||
state.set_cursor_range(Some(cursor_range));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -593,7 +559,7 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cursor_range = None;
|
let mut cursor_range = None;
|
||||||
let prev_cursor_range = state.cursor_range(&galley);
|
let prev_cursor_range = state.cursor.range(&galley);
|
||||||
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
if interactive && ui.memory(|mem| mem.has_focus(id)) {
|
||||||
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
|
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
|
||||||
|
|
||||||
|
|
@ -624,11 +590,11 @@ impl<'t> TextEdit<'t> {
|
||||||
cursor_range = Some(new_cursor_range);
|
cursor_range = Some(new_cursor_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text_draw_pos = align
|
let mut galley_pos = align
|
||||||
.align_size_within_rect(galley.size(), response.rect)
|
.align_size_within_rect(galley.size(), response.rect)
|
||||||
.intersect(response.rect) // limit pos to the response rect area
|
.intersect(response.rect) // limit pos to the response rect area
|
||||||
.min;
|
.min;
|
||||||
let align_offset = response.rect.left() - text_draw_pos.x;
|
let align_offset = response.rect.left() - galley_pos.x;
|
||||||
|
|
||||||
// Visual clipping for singleline text editor with text larger than width
|
// Visual clipping for singleline text editor with text larger than width
|
||||||
if clip_text && align_offset == 0.0 {
|
if clip_text && align_offset == 0.0 {
|
||||||
|
|
@ -653,7 +619,7 @@ impl<'t> TextEdit<'t> {
|
||||||
.at_least(0.0);
|
.at_least(0.0);
|
||||||
|
|
||||||
state.singleline_offset = offset_x;
|
state.singleline_offset = offset_x;
|
||||||
text_draw_pos -= vec2(offset_x, 0.0);
|
galley_pos -= vec2(offset_x, 0.0);
|
||||||
} else {
|
} else {
|
||||||
state.singleline_offset = align_offset;
|
state.singleline_offset = align_offset;
|
||||||
}
|
}
|
||||||
|
|
@ -667,7 +633,7 @@ impl<'t> TextEdit<'t> {
|
||||||
};
|
};
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
if ui.is_rect_visible(rect) {
|
||||||
painter.galley(text_draw_pos, galley.clone(), text_color);
|
painter.galley(galley_pos, galley.clone(), text_color);
|
||||||
|
|
||||||
if text.as_str().is_empty() && !hint_text.is_empty() {
|
if text.as_str().is_empty() && !hint_text.is_empty() {
|
||||||
let hint_text_color = ui.visuals().weak_text_color();
|
let hint_text_color = ui.visuals().weak_text_color();
|
||||||
|
|
@ -680,30 +646,36 @@ impl<'t> TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory(|mem| mem.has_focus(id)) {
|
if ui.memory(|mem| mem.has_focus(id)) {
|
||||||
if let Some(cursor_range) = state.cursor_range(&galley) {
|
if let Some(cursor_range) = state.cursor.range(&galley) {
|
||||||
// We paint the cursor on top of the text, in case
|
// We paint the cursor on top of the text, in case
|
||||||
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
// the text galley has backgrounds (as e.g. `code` snippets in markup do).
|
||||||
paint_cursor_selection(ui, &painter, text_draw_pos, &galley, &cursor_range);
|
paint_cursor_selection(
|
||||||
|
ui.visuals(),
|
||||||
|
&painter,
|
||||||
|
galley_pos,
|
||||||
|
&galley,
|
||||||
|
&cursor_range,
|
||||||
|
);
|
||||||
|
|
||||||
|
let primary_cursor_rect =
|
||||||
|
cursor_rect(galley_pos, &galley, &cursor_range.primary, row_height);
|
||||||
|
|
||||||
|
let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531
|
||||||
|
if (response.changed || selection_changed) && !is_fully_visible {
|
||||||
|
// Scroll to keep primary cursor in view:
|
||||||
|
ui.scroll_to_rect(primary_cursor_rect, None);
|
||||||
|
}
|
||||||
|
|
||||||
if text.is_mutable() {
|
if text.is_mutable() {
|
||||||
let cursor_rect = paint_cursor_end(
|
paint_cursor(&painter, ui.visuals(), primary_cursor_rect);
|
||||||
ui,
|
|
||||||
row_height,
|
|
||||||
&painter,
|
|
||||||
text_draw_pos,
|
|
||||||
&galley,
|
|
||||||
&cursor_range.primary,
|
|
||||||
);
|
|
||||||
|
|
||||||
let is_fully_visible = ui.clip_rect().contains_rect(rect); // TODO: remove this HACK workaround for https://github.com/emilk/egui/issues/1531
|
|
||||||
if (response.changed || selection_changed) && !is_fully_visible {
|
|
||||||
ui.scroll_to_rect(cursor_rect, None); // keep cursor in view
|
|
||||||
}
|
|
||||||
|
|
||||||
if interactive {
|
if interactive {
|
||||||
// For IME, so only set it when text is editable and visible!
|
// For IME, so only set it when text is editable and visible!
|
||||||
ui.ctx().output_mut(|o| {
|
ui.ctx().output_mut(|o| {
|
||||||
o.ime = Some(crate::output::IMEOutput { rect, cursor_rect });
|
o.ime = Some(crate::output::IMEOutput {
|
||||||
|
rect,
|
||||||
|
cursor_rect: primary_cursor_rect,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -740,102 +712,28 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
#[cfg(feature = "accesskit")]
|
||||||
{
|
{
|
||||||
let parent_id = ui.ctx().accesskit_node_builder(response.id, |builder| {
|
let role = if password {
|
||||||
use accesskit::{TextPosition, TextSelection};
|
accesskit::Role::PasswordInput
|
||||||
|
} else if multiline {
|
||||||
|
accesskit::Role::MultilineTextInput
|
||||||
|
} else {
|
||||||
|
accesskit::Role::TextInput
|
||||||
|
};
|
||||||
|
|
||||||
let parent_id = response.id;
|
super::accesskit_text::update_accesskit_for_text_widget(
|
||||||
|
ui.ctx(),
|
||||||
if let Some(cursor_range) = &cursor_range {
|
id,
|
||||||
let anchor = &cursor_range.secondary.rcursor;
|
cursor_range,
|
||||||
let focus = &cursor_range.primary.rcursor;
|
role,
|
||||||
builder.set_text_selection(TextSelection {
|
galley_pos,
|
||||||
anchor: TextPosition {
|
&galley,
|
||||||
node: parent_id.with(anchor.row).accesskit_id(),
|
);
|
||||||
character_index: anchor.column,
|
|
||||||
},
|
|
||||||
focus: TextPosition {
|
|
||||||
node: parent_id.with(focus.row).accesskit_id(),
|
|
||||||
character_index: focus.column,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.set_default_action_verb(accesskit::DefaultActionVerb::Focus);
|
|
||||||
if self.multiline {
|
|
||||||
builder.set_role(Role::MultilineTextInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
parent_id
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(parent_id) = parent_id {
|
|
||||||
// drop ctx lock before further processing
|
|
||||||
use accesskit::TextDirection;
|
|
||||||
|
|
||||||
ui.ctx().with_accessibility_parent(parent_id, || {
|
|
||||||
for (i, row) in galley.rows.iter().enumerate() {
|
|
||||||
let id = parent_id.with(i);
|
|
||||||
ui.ctx().accesskit_node_builder(id, |builder| {
|
|
||||||
builder.set_role(Role::InlineTextBox);
|
|
||||||
let rect = row.rect.translate(text_draw_pos.to_vec2());
|
|
||||||
builder.set_bounds(accesskit::Rect {
|
|
||||||
x0: rect.min.x.into(),
|
|
||||||
y0: rect.min.y.into(),
|
|
||||||
x1: rect.max.x.into(),
|
|
||||||
y1: rect.max.y.into(),
|
|
||||||
});
|
|
||||||
builder.set_text_direction(TextDirection::LeftToRight);
|
|
||||||
// TODO(mwcampbell): Set more node fields for the row
|
|
||||||
// once AccessKit adapters expose text formatting info.
|
|
||||||
|
|
||||||
let glyph_count = row.glyphs.len();
|
|
||||||
let mut value = String::new();
|
|
||||||
value.reserve(glyph_count);
|
|
||||||
let mut character_lengths = Vec::<u8>::with_capacity(glyph_count);
|
|
||||||
let mut character_positions = Vec::<f32>::with_capacity(glyph_count);
|
|
||||||
let mut character_widths = Vec::<f32>::with_capacity(glyph_count);
|
|
||||||
let mut word_lengths = Vec::<u8>::new();
|
|
||||||
let mut was_at_word_end = false;
|
|
||||||
let mut last_word_start = 0usize;
|
|
||||||
|
|
||||||
for glyph in &row.glyphs {
|
|
||||||
let is_word_char = is_word_char(glyph.chr);
|
|
||||||
if is_word_char && was_at_word_end {
|
|
||||||
word_lengths
|
|
||||||
.push((character_lengths.len() - last_word_start) as _);
|
|
||||||
last_word_start = character_lengths.len();
|
|
||||||
}
|
|
||||||
was_at_word_end = !is_word_char;
|
|
||||||
let old_len = value.len();
|
|
||||||
value.push(glyph.chr);
|
|
||||||
character_lengths.push((value.len() - old_len) as _);
|
|
||||||
character_positions.push(glyph.pos.x - row.rect.min.x);
|
|
||||||
character_widths.push(glyph.size.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
if row.ends_with_newline {
|
|
||||||
value.push('\n');
|
|
||||||
character_lengths.push(1);
|
|
||||||
character_positions.push(row.rect.max.x - row.rect.min.x);
|
|
||||||
character_widths.push(0.0);
|
|
||||||
}
|
|
||||||
word_lengths.push((character_lengths.len() - last_word_start) as _);
|
|
||||||
|
|
||||||
builder.set_value(value);
|
|
||||||
builder.set_character_lengths(character_lengths);
|
|
||||||
builder.set_character_positions(character_positions);
|
|
||||||
builder.set_character_widths(character_widths);
|
|
||||||
builder.set_word_lengths(word_lengths);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditOutput {
|
TextEditOutput {
|
||||||
response,
|
response,
|
||||||
galley,
|
galley,
|
||||||
text_draw_pos,
|
galley_pos,
|
||||||
text_clip_rect,
|
text_clip_rect,
|
||||||
state,
|
state,
|
||||||
cursor_range,
|
cursor_range,
|
||||||
|
|
@ -859,28 +757,6 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
fn ccursor_from_accesskit_text_position(
|
|
||||||
id: Id,
|
|
||||||
galley: &Galley,
|
|
||||||
position: &accesskit::TextPosition,
|
|
||||||
) -> Option<CCursor> {
|
|
||||||
let mut total_length = 0usize;
|
|
||||||
for (i, row) in galley.rows.iter().enumerate() {
|
|
||||||
let row_id = id.with(i);
|
|
||||||
if row_id.accesskit_id() == position.node {
|
|
||||||
return Some(CCursor {
|
|
||||||
index: total_length + position.character_index,
|
|
||||||
prefer_next_row: !(position.character_index == row.glyphs.len()
|
|
||||||
&& !row.ends_with_newline
|
|
||||||
&& (i + 1) < galley.rows.len()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check for (keyboard) events to edit the cursor and/or text.
|
/// Check for (keyboard) events to edit the cursor and/or text.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn events(
|
fn events(
|
||||||
|
|
@ -897,7 +773,7 @@ fn events(
|
||||||
char_limit: usize,
|
char_limit: usize,
|
||||||
event_filter: EventFilter,
|
event_filter: EventFilter,
|
||||||
) -> (bool, CursorRange) {
|
) -> (bool, CursorRange) {
|
||||||
let mut cursor_range = state.cursor_range(galley).unwrap_or(default_cursor_range);
|
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
|
||||||
|
|
||||||
// We feed state to the undoer both before and after handling input
|
// We feed state to the undoer both before and after handling input
|
||||||
// so that the undoer creates automatic saves even when there are no events for a while.
|
// so that the undoer creates automatic saves even when there are no events for a while.
|
||||||
|
|
@ -917,11 +793,14 @@ fn events(
|
||||||
let events = ui.input(|i| i.filtered_events(&event_filter));
|
let events = ui.input(|i| i.filtered_events(&event_filter));
|
||||||
for event in &events {
|
for event in &events {
|
||||||
let did_mutate_text = match event {
|
let did_mutate_text = match event {
|
||||||
|
// First handle events that only changes the selection cursor, not the text:
|
||||||
|
event if cursor_range.on_event(event, galley, id) => None,
|
||||||
|
|
||||||
Event::Copy => {
|
Event::Copy => {
|
||||||
if cursor_range.is_empty() {
|
if cursor_range.is_empty() {
|
||||||
copy_if_not_password(ui, text.as_str().to_owned());
|
copy_if_not_password(ui, text.as_str().to_owned());
|
||||||
} else {
|
} else {
|
||||||
copy_if_not_password(ui, selected_str(text, &cursor_range).to_owned());
|
copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -930,7 +809,7 @@ fn events(
|
||||||
copy_if_not_password(ui, text.take());
|
copy_if_not_password(ui, text.take());
|
||||||
Some(CCursorRange::default())
|
Some(CCursorRange::default())
|
||||||
} else {
|
} else {
|
||||||
copy_if_not_password(ui, selected_str(text, &cursor_range).to_owned());
|
copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned());
|
||||||
Some(CCursorRange::one(delete_selected(text, &cursor_range)))
|
Some(CCursorRange::one(delete_selected(text, &cursor_range)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1026,11 +905,11 @@ fn events(
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::Key {
|
Event::Key {
|
||||||
|
modifiers,
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
|
||||||
..
|
..
|
||||||
} => on_key_press(&mut cursor_range, text, galley, *key, modifiers),
|
} => check_for_mutating_key_press(&mut cursor_range, text, galley, modifiers, *key),
|
||||||
|
|
||||||
Event::CompositionStart => {
|
Event::CompositionStart => {
|
||||||
state.has_ime = true;
|
state.has_ime = true;
|
||||||
|
|
@ -1066,27 +945,6 @@ fn events(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "accesskit")]
|
|
||||||
Event::AccessKitActionRequest(accesskit::ActionRequest {
|
|
||||||
action: accesskit::Action::SetTextSelection,
|
|
||||||
target,
|
|
||||||
data: Some(accesskit::ActionData::SetTextSelection(selection)),
|
|
||||||
}) => {
|
|
||||||
if id.accesskit_id() == *target {
|
|
||||||
let primary =
|
|
||||||
ccursor_from_accesskit_text_position(id, galley, &selection.focus);
|
|
||||||
let secondary =
|
|
||||||
ccursor_from_accesskit_text_position(id, galley, &selection.anchor);
|
|
||||||
if let (Some(primary), Some(secondary)) = (primary, secondary) {
|
|
||||||
Some(CCursorRange { primary, secondary })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1104,7 +962,7 @@ fn events(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.set_cursor_range(Some(cursor_range));
|
state.cursor.set_range(Some(cursor_range));
|
||||||
|
|
||||||
state.undoer.lock().feed_state(
|
state.undoer.lock().feed_state(
|
||||||
ui.input(|i| i.time),
|
ui.input(|i| i.time),
|
||||||
|
|
@ -1116,10 +974,10 @@ fn events(
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
fn paint_cursor_selection(
|
pub fn paint_cursor_selection(
|
||||||
ui: &Ui,
|
visuals: &Visuals,
|
||||||
painter: &Painter,
|
painter: &Painter,
|
||||||
pos: Pos2,
|
galley_pos: Pos2,
|
||||||
galley: &Galley,
|
galley: &Galley,
|
||||||
cursor_range: &CursorRange,
|
cursor_range: &CursorRange,
|
||||||
) {
|
) {
|
||||||
|
|
@ -1128,7 +986,7 @@ fn paint_cursor_selection(
|
||||||
}
|
}
|
||||||
|
|
||||||
// We paint the cursor selection on top of the text, so make it transparent:
|
// We paint the cursor selection on top of the text, so make it transparent:
|
||||||
let color = ui.visuals().selection.bg_fill.linear_multiply(0.5);
|
let color = visuals.selection.bg_fill.linear_multiply(0.5);
|
||||||
let [min, max] = cursor_range.sorted_cursors();
|
let [min, max] = cursor_range.sorted_cursors();
|
||||||
let min = min.rcursor;
|
let min = min.rcursor;
|
||||||
let max = max.rcursor;
|
let max = max.rcursor;
|
||||||
|
|
@ -1151,29 +1009,19 @@ fn paint_cursor_selection(
|
||||||
row.rect.right() + newline_size
|
row.rect.right() + newline_size
|
||||||
};
|
};
|
||||||
let rect = Rect::from_min_max(
|
let rect = Rect::from_min_max(
|
||||||
pos + vec2(left, row.min_y()),
|
galley_pos + vec2(left, row.min_y()),
|
||||||
pos + vec2(right, row.max_y()),
|
galley_pos + vec2(right, row.max_y()),
|
||||||
);
|
);
|
||||||
painter.rect_filled(rect, 0.0, color);
|
painter.rect_filled(rect, 0.0, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint_cursor_end(
|
/// Paint one end of the selection, e.g. the primary cursor.
|
||||||
ui: &Ui,
|
fn paint_cursor(painter: &Painter, visuals: &Visuals, cursor_rect: Rect) {
|
||||||
row_height: f32,
|
let stroke = visuals.text_cursor;
|
||||||
painter: &Painter,
|
|
||||||
pos: Pos2,
|
|
||||||
galley: &Galley,
|
|
||||||
cursor: &Cursor,
|
|
||||||
) -> Rect {
|
|
||||||
let stroke = ui.visuals().text_cursor;
|
|
||||||
|
|
||||||
let mut cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
|
let top = cursor_rect.center_top();
|
||||||
cursor_pos.max.y = cursor_pos.max.y.at_least(cursor_pos.min.y + row_height); // Handle completely empty galleys
|
let bottom = cursor_rect.center_bottom();
|
||||||
cursor_pos = cursor_pos.expand(1.5); // slightly above/below row
|
|
||||||
|
|
||||||
let top = cursor_pos.center_top();
|
|
||||||
let bottom = cursor_pos.center_bottom();
|
|
||||||
|
|
||||||
painter.line_segment([top, bottom], (stroke.width, stroke.color));
|
painter.line_segment([top, bottom], (stroke.width, stroke.color));
|
||||||
|
|
||||||
|
|
@ -1190,17 +1038,10 @@ fn paint_cursor_end(
|
||||||
(width, stroke.color),
|
(width, stroke.color),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor_pos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
fn selected_str<'s>(text: &'s dyn TextBuffer, cursor_range: &CursorRange) -> &'s str {
|
|
||||||
let [min, max] = cursor_range.sorted_cursors();
|
|
||||||
text.char_range(min.ccursor.index..max.ccursor.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_text(
|
fn insert_text(
|
||||||
ccursor: &mut CCursor,
|
ccursor: &mut CCursor,
|
||||||
text: &mut dyn TextBuffer,
|
text: &mut dyn TextBuffer,
|
||||||
|
|
@ -1301,12 +1142,12 @@ fn delete_paragraph_after_cursor(
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Returns `Some(new_cursor)` if we did mutate `text`.
|
/// Returns `Some(new_cursor)` if we did mutate `text`.
|
||||||
fn on_key_press(
|
fn check_for_mutating_key_press(
|
||||||
cursor_range: &mut CursorRange,
|
cursor_range: &mut CursorRange,
|
||||||
text: &mut dyn TextBuffer,
|
text: &mut dyn TextBuffer,
|
||||||
galley: &Galley,
|
galley: &Galley,
|
||||||
key: Key,
|
|
||||||
modifiers: &Modifiers,
|
modifiers: &Modifiers,
|
||||||
|
key: Key,
|
||||||
) -> Option<CCursorRange> {
|
) -> Option<CCursorRange> {
|
||||||
match key {
|
match key {
|
||||||
Key::Backspace => {
|
Key::Backspace => {
|
||||||
|
|
@ -1324,6 +1165,7 @@ fn on_key_press(
|
||||||
};
|
};
|
||||||
Some(CCursorRange::one(ccursor))
|
Some(CCursorRange::one(ccursor))
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::Delete if !modifiers.shift || !cfg!(target_os = "windows") => {
|
Key::Delete if !modifiers.shift || !cfg!(target_os = "windows") => {
|
||||||
let ccursor = if modifiers.mac_cmd {
|
let ccursor = if modifiers.mac_cmd {
|
||||||
delete_paragraph_after_cursor(text, galley, cursor_range)
|
delete_paragraph_after_cursor(text, galley, cursor_range)
|
||||||
|
|
@ -1344,12 +1186,6 @@ fn on_key_press(
|
||||||
Some(CCursorRange::one(ccursor))
|
Some(CCursorRange::one(ccursor))
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::A if modifiers.command => {
|
|
||||||
// select all
|
|
||||||
*cursor_range = CursorRange::two(Cursor::default(), galley.end());
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::H if modifiers.ctrl => {
|
Key::H if modifiers.ctrl => {
|
||||||
let ccursor = delete_previous_char(text, cursor_range.primary.ccursor);
|
let ccursor = delete_previous_char(text, cursor_range.primary.ccursor);
|
||||||
Some(CCursorRange::one(ccursor))
|
Some(CCursorRange::one(ccursor))
|
||||||
|
|
@ -1374,276 +1210,12 @@ fn on_key_press(
|
||||||
Some(CCursorRange::one(ccursor))
|
Some(CCursorRange::one(ccursor))
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::ArrowLeft | Key::ArrowRight if modifiers.is_none() && !cursor_range.is_empty() => {
|
|
||||||
if key == Key::ArrowLeft {
|
|
||||||
*cursor_range = CursorRange::one(cursor_range.sorted_cursors()[0]);
|
|
||||||
} else {
|
|
||||||
*cursor_range = CursorRange::one(cursor_range.sorted_cursors()[1]);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End => {
|
|
||||||
move_single_cursor(&mut cursor_range.primary, galley, key, modifiers);
|
|
||||||
if !modifiers.shift {
|
|
||||||
cursor_range.secondary = cursor_range.primary;
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::P | Key::N | Key::B | Key::F | Key::A | Key::E
|
|
||||||
if cfg!(target_os = "macos") && modifiers.ctrl && !modifiers.shift =>
|
|
||||||
{
|
|
||||||
move_single_cursor(&mut cursor_range.primary, galley, key, modifiers);
|
|
||||||
cursor_range.secondary = cursor_range.primary;
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) {
|
|
||||||
if cfg!(target_os = "macos") && modifiers.ctrl && !modifiers.shift {
|
|
||||||
match key {
|
|
||||||
Key::A => *cursor = galley.cursor_begin_of_row(cursor),
|
|
||||||
Key::E => *cursor = galley.cursor_end_of_row(cursor),
|
|
||||||
Key::P => *cursor = galley.cursor_up_one_row(cursor),
|
|
||||||
Key::N => *cursor = galley.cursor_down_one_row(cursor),
|
|
||||||
Key::B => *cursor = galley.cursor_left_one_character(cursor),
|
|
||||||
Key::F => *cursor = galley.cursor_right_one_character(cursor),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match key {
|
|
||||||
Key::ArrowLeft => {
|
|
||||||
if modifiers.alt || modifiers.ctrl {
|
|
||||||
// alt on mac, ctrl on windows
|
|
||||||
*cursor = galley.from_ccursor(ccursor_previous_word(galley.text(), cursor.ccursor));
|
|
||||||
} else if modifiers.mac_cmd {
|
|
||||||
*cursor = galley.cursor_begin_of_row(cursor);
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_left_one_character(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::ArrowRight => {
|
|
||||||
if modifiers.alt || modifiers.ctrl {
|
|
||||||
// alt on mac, ctrl on windows
|
|
||||||
*cursor = galley.from_ccursor(ccursor_next_word(galley.text(), cursor.ccursor));
|
|
||||||
} else if modifiers.mac_cmd {
|
|
||||||
*cursor = galley.cursor_end_of_row(cursor);
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_right_one_character(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::ArrowUp => {
|
|
||||||
if modifiers.command {
|
|
||||||
// mac and windows behavior
|
|
||||||
*cursor = Cursor::default();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_up_one_row(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::ArrowDown => {
|
|
||||||
if modifiers.command {
|
|
||||||
// mac and windows behavior
|
|
||||||
*cursor = galley.end();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_down_one_row(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::Home => {
|
|
||||||
if modifiers.ctrl {
|
|
||||||
// windows behavior
|
|
||||||
*cursor = Cursor::default();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_begin_of_row(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::End => {
|
|
||||||
if modifiers.ctrl {
|
|
||||||
// windows behavior
|
|
||||||
*cursor = galley.end();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_end_of_row(cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
|
|
||||||
if ccursor.index == 0 {
|
|
||||||
CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
|
|
||||||
} else {
|
|
||||||
let it = text.chars();
|
|
||||||
let mut it = it.skip(ccursor.index - 1);
|
|
||||||
if let Some(char_before_cursor) = it.next() {
|
|
||||||
if let Some(char_after_cursor) = it.next() {
|
|
||||||
if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
|
|
||||||
let min = ccursor_previous_word(text, ccursor + 1);
|
|
||||||
let max = ccursor_next_word(text, min);
|
|
||||||
CCursorRange::two(min, max)
|
|
||||||
} else if is_word_char(char_before_cursor) {
|
|
||||||
let min = ccursor_previous_word(text, ccursor);
|
|
||||||
let max = ccursor_next_word(text, min);
|
|
||||||
CCursorRange::two(min, max)
|
|
||||||
} else if is_word_char(char_after_cursor) {
|
|
||||||
let max = ccursor_next_word(text, ccursor);
|
|
||||||
CCursorRange::two(ccursor, max)
|
|
||||||
} else {
|
|
||||||
let min = ccursor_previous_word(text, ccursor);
|
|
||||||
let max = ccursor_next_word(text, ccursor);
|
|
||||||
CCursorRange::two(min, max)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let min = ccursor_previous_word(text, ccursor);
|
|
||||||
CCursorRange::two(min, ccursor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let max = ccursor_next_word(text, ccursor);
|
|
||||||
CCursorRange::two(ccursor, max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
|
|
||||||
if ccursor.index == 0 {
|
|
||||||
CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
|
|
||||||
} else {
|
|
||||||
let it = text.chars();
|
|
||||||
let mut it = it.skip(ccursor.index - 1);
|
|
||||||
if let Some(char_before_cursor) = it.next() {
|
|
||||||
if let Some(char_after_cursor) = it.next() {
|
|
||||||
if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
|
|
||||||
let min = ccursor_previous_line(text, ccursor + 1);
|
|
||||||
let max = ccursor_next_line(text, min);
|
|
||||||
CCursorRange::two(min, max)
|
|
||||||
} else if !is_linebreak(char_before_cursor) {
|
|
||||||
let min = ccursor_previous_line(text, ccursor);
|
|
||||||
let max = ccursor_next_line(text, min);
|
|
||||||
CCursorRange::two(min, max)
|
|
||||||
} else if !is_linebreak(char_after_cursor) {
|
|
||||||
let max = ccursor_next_line(text, ccursor);
|
|
||||||
CCursorRange::two(ccursor, max)
|
|
||||||
} else {
|
|
||||||
let min = ccursor_previous_line(text, ccursor);
|
|
||||||
let max = ccursor_next_line(text, ccursor);
|
|
||||||
CCursorRange::two(min, max)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let min = ccursor_previous_line(text, ccursor);
|
|
||||||
CCursorRange::two(min, ccursor)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let max = ccursor_next_line(text, ccursor);
|
|
||||||
CCursorRange::two(ccursor, max)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
|
|
||||||
CCursor {
|
|
||||||
index: next_word_boundary_char_index(text.chars(), ccursor.index),
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
|
|
||||||
CCursor {
|
|
||||||
index: next_line_boundary_char_index(text.chars(), ccursor.index),
|
|
||||||
prefer_next_row: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
|
|
||||||
let num_chars = text.chars().count();
|
|
||||||
CCursor {
|
|
||||||
index: num_chars
|
|
||||||
- next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
|
|
||||||
prefer_next_row: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
|
|
||||||
let num_chars = text.chars().count();
|
|
||||||
CCursor {
|
|
||||||
index: num_chars
|
|
||||||
- next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
|
|
||||||
prefer_next_row: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
|
||||||
let mut it = it.skip(index);
|
|
||||||
if let Some(_first) = it.next() {
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
if let Some(second) = it.next() {
|
|
||||||
index += 1;
|
|
||||||
for next in it {
|
|
||||||
if is_word_char(next) != is_word_char(second) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
|
||||||
let mut it = it.skip(index);
|
|
||||||
if let Some(_first) = it.next() {
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
if let Some(second) = it.next() {
|
|
||||||
index += 1;
|
|
||||||
for next in it {
|
|
||||||
if is_linebreak(next) != is_linebreak(second) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_word_char(c: char) -> bool {
|
|
||||||
c.is_ascii_alphanumeric() || c == '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_linebreak(c: char) -> bool {
|
|
||||||
c == '\r' || c == '\n'
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accepts and returns character offset (NOT byte offset!).
|
|
||||||
fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
|
|
||||||
// We know that new lines, '\n', are a single byte char, but we have to
|
|
||||||
// work with char offsets because before the new line there may be any
|
|
||||||
// number of multi byte chars.
|
|
||||||
// We need to know the char index to be able to correctly set the cursor
|
|
||||||
// later.
|
|
||||||
let chars_count = text.chars().count();
|
|
||||||
|
|
||||||
let position = text
|
|
||||||
.chars()
|
|
||||||
.rev()
|
|
||||||
.skip(chars_count - current_index.index)
|
|
||||||
.position(|x| x == '\n');
|
|
||||||
|
|
||||||
match position {
|
|
||||||
Some(pos) => CCursor::new(current_index.index - pos),
|
|
||||||
None => CCursor::new(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decrease_indentation(ccursor: &mut CCursor, text: &mut dyn TextBuffer) {
|
fn decrease_indentation(ccursor: &mut CCursor, text: &mut dyn TextBuffer) {
|
||||||
let line_start = find_line_start(text.as_str(), *ccursor);
|
let line_start = find_line_start(text.as_str(), *ccursor);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
//! Text cursor changes/interaction, without modifying the text.
|
||||||
|
|
||||||
|
use epaint::text::{cursor::*, Galley};
|
||||||
|
use text_edit::state::TextCursorState;
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
use super::{CCursorRange, CursorRange};
|
||||||
|
|
||||||
|
impl TextCursorState {
|
||||||
|
/// Handle clicking and/or dragging text.
|
||||||
|
///
|
||||||
|
/// Returns `true` if there was interaction.
|
||||||
|
pub fn pointer_interaction(
|
||||||
|
&mut self,
|
||||||
|
ui: &Ui,
|
||||||
|
response: &Response,
|
||||||
|
cursor_at_pointer: Cursor,
|
||||||
|
galley: &Galley,
|
||||||
|
) -> bool {
|
||||||
|
let text = galley.text();
|
||||||
|
|
||||||
|
if response.double_clicked() {
|
||||||
|
// Select word:
|
||||||
|
let ccursor_range = select_word_at(text, cursor_at_pointer.ccursor);
|
||||||
|
self.set_range(Some(CursorRange {
|
||||||
|
primary: galley.from_ccursor(ccursor_range.primary),
|
||||||
|
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||||
|
}));
|
||||||
|
true
|
||||||
|
} else if response.triple_clicked() {
|
||||||
|
// Select line:
|
||||||
|
let ccursor_range = select_line_at(text, cursor_at_pointer.ccursor);
|
||||||
|
self.set_range(Some(CursorRange {
|
||||||
|
primary: galley.from_ccursor(ccursor_range.primary),
|
||||||
|
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||||
|
}));
|
||||||
|
true
|
||||||
|
} else if response.sense.drag {
|
||||||
|
if response.hovered() && ui.input(|i| i.pointer.any_pressed()) {
|
||||||
|
if ui.input(|i| i.modifiers.shift) {
|
||||||
|
if let Some(mut cursor_range) = self.range(galley) {
|
||||||
|
cursor_range.primary = cursor_at_pointer;
|
||||||
|
self.set_range(Some(cursor_range));
|
||||||
|
} else {
|
||||||
|
self.set_range(Some(CursorRange::one(cursor_at_pointer)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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:
|
||||||
|
if let Some(mut cursor_range) = self.range(galley) {
|
||||||
|
cursor_range.primary = cursor_at_pointer;
|
||||||
|
self.set_range(Some(cursor_range));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
|
||||||
|
if ccursor.index == 0 {
|
||||||
|
CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
|
||||||
|
} else {
|
||||||
|
let it = text.chars();
|
||||||
|
let mut it = it.skip(ccursor.index - 1);
|
||||||
|
if let Some(char_before_cursor) = it.next() {
|
||||||
|
if let Some(char_after_cursor) = it.next() {
|
||||||
|
if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
|
||||||
|
let min = ccursor_previous_word(text, ccursor + 1);
|
||||||
|
let max = ccursor_next_word(text, min);
|
||||||
|
CCursorRange::two(min, max)
|
||||||
|
} else if is_word_char(char_before_cursor) {
|
||||||
|
let min = ccursor_previous_word(text, ccursor);
|
||||||
|
let max = ccursor_next_word(text, min);
|
||||||
|
CCursorRange::two(min, max)
|
||||||
|
} else if is_word_char(char_after_cursor) {
|
||||||
|
let max = ccursor_next_word(text, ccursor);
|
||||||
|
CCursorRange::two(ccursor, max)
|
||||||
|
} else {
|
||||||
|
let min = ccursor_previous_word(text, ccursor);
|
||||||
|
let max = ccursor_next_word(text, ccursor);
|
||||||
|
CCursorRange::two(min, max)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let min = ccursor_previous_word(text, ccursor);
|
||||||
|
CCursorRange::two(min, ccursor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let max = ccursor_next_word(text, ccursor);
|
||||||
|
CCursorRange::two(ccursor, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
|
||||||
|
if ccursor.index == 0 {
|
||||||
|
CCursorRange::two(ccursor, ccursor_next_line(text, ccursor))
|
||||||
|
} else {
|
||||||
|
let it = text.chars();
|
||||||
|
let mut it = it.skip(ccursor.index - 1);
|
||||||
|
if let Some(char_before_cursor) = it.next() {
|
||||||
|
if let Some(char_after_cursor) = it.next() {
|
||||||
|
if (!is_linebreak(char_before_cursor)) && (!is_linebreak(char_after_cursor)) {
|
||||||
|
let min = ccursor_previous_line(text, ccursor + 1);
|
||||||
|
let max = ccursor_next_line(text, min);
|
||||||
|
CCursorRange::two(min, max)
|
||||||
|
} else if !is_linebreak(char_before_cursor) {
|
||||||
|
let min = ccursor_previous_line(text, ccursor);
|
||||||
|
let max = ccursor_next_line(text, min);
|
||||||
|
CCursorRange::two(min, max)
|
||||||
|
} else if !is_linebreak(char_after_cursor) {
|
||||||
|
let max = ccursor_next_line(text, ccursor);
|
||||||
|
CCursorRange::two(ccursor, max)
|
||||||
|
} else {
|
||||||
|
let min = ccursor_previous_line(text, ccursor);
|
||||||
|
let max = ccursor_next_line(text, ccursor);
|
||||||
|
CCursorRange::two(min, max)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let min = ccursor_previous_line(text, ccursor);
|
||||||
|
CCursorRange::two(min, ccursor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let max = ccursor_next_line(text, ccursor);
|
||||||
|
CCursorRange::two(ccursor, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
|
CCursor {
|
||||||
|
index: next_word_boundary_char_index(text.chars(), ccursor.index),
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ccursor_next_line(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
|
CCursor {
|
||||||
|
index: next_line_boundary_char_index(text.chars(), ccursor.index),
|
||||||
|
prefer_next_row: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
|
let num_chars = text.chars().count();
|
||||||
|
CCursor {
|
||||||
|
index: num_chars
|
||||||
|
- next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
|
||||||
|
prefer_next_row: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
|
let num_chars = text.chars().count();
|
||||||
|
CCursor {
|
||||||
|
index: num_chars
|
||||||
|
- next_line_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
|
||||||
|
prefer_next_row: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||||
|
let mut it = it.skip(index);
|
||||||
|
if let Some(_first) = it.next() {
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
if let Some(second) = it.next() {
|
||||||
|
index += 1;
|
||||||
|
for next in it {
|
||||||
|
if is_word_char(next) != is_word_char(second) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_line_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
|
||||||
|
let mut it = it.skip(index);
|
||||||
|
if let Some(_first) = it.next() {
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
if let Some(second) = it.next() {
|
||||||
|
index += 1;
|
||||||
|
for next in it {
|
||||||
|
if is_linebreak(next) != is_linebreak(second) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_word_char(c: char) -> bool {
|
||||||
|
c.is_ascii_alphanumeric() || c == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_linebreak(c: char) -> bool {
|
||||||
|
c == '\r' || c == '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Accepts and returns character offset (NOT byte offset!).
|
||||||
|
pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
|
||||||
|
// We know that new lines, '\n', are a single byte char, but we have to
|
||||||
|
// work with char offsets because before the new line there may be any
|
||||||
|
// number of multi byte chars.
|
||||||
|
// We need to know the char index to be able to correctly set the cursor
|
||||||
|
// later.
|
||||||
|
let chars_count = text.chars().count();
|
||||||
|
|
||||||
|
let position = text
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.skip(chars_count - current_index.index)
|
||||||
|
.position(|x| x == '\n');
|
||||||
|
|
||||||
|
match position {
|
||||||
|
Some(pos) => CCursor::new(current_index.index - pos),
|
||||||
|
None => CCursor::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
|
||||||
|
for (ci, (bi, _)) in s.char_indices().enumerate() {
|
||||||
|
if ci == char_index {
|
||||||
|
return bi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slice_char_range(s: &str, char_range: std::ops::Range<usize>) -> &str {
|
||||||
|
assert!(char_range.start <= char_range.end);
|
||||||
|
let start_byte = byte_index_from_char_index(s, char_range.start);
|
||||||
|
let end_byte = byte_index_from_char_index(s, char_range.end);
|
||||||
|
&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);
|
||||||
|
// Handle completely empty galleys
|
||||||
|
cursor_pos = cursor_pos.expand(1.5);
|
||||||
|
// slightly above/below row
|
||||||
|
cursor_pos
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
use epaint::text::cursor::*;
|
use epaint::{text::cursor::*, Galley};
|
||||||
|
|
||||||
|
use crate::{Event, Id, Key, Modifiers};
|
||||||
|
|
||||||
|
use super::cursor_interaction::{ccursor_next_word, ccursor_previous_word, slice_char_range};
|
||||||
|
|
||||||
/// A selected text range (could be a range of length zero).
|
/// A selected text range (could be a range of length zero).
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||||
|
|
@ -16,6 +20,7 @@ pub struct CursorRange {
|
||||||
|
|
||||||
impl CursorRange {
|
impl CursorRange {
|
||||||
/// The empty range.
|
/// The empty range.
|
||||||
|
#[inline]
|
||||||
pub fn one(cursor: Cursor) -> Self {
|
pub fn one(cursor: Cursor) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primary: cursor,
|
primary: cursor,
|
||||||
|
|
@ -23,6 +28,7 @@ impl CursorRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn two(min: Cursor, max: Cursor) -> Self {
|
pub fn two(min: Cursor, max: Cursor) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primary: max,
|
primary: max,
|
||||||
|
|
@ -30,6 +36,11 @@ impl CursorRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select all the text in a galley
|
||||||
|
pub fn select_all(galley: &Galley) -> Self {
|
||||||
|
Self::two(Cursor::default(), galley.end())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn as_ccursor_range(&self) -> CCursorRange {
|
pub fn as_ccursor_range(&self) -> CCursorRange {
|
||||||
CCursorRange {
|
CCursorRange {
|
||||||
primary: self.primary.ccursor,
|
primary: self.primary.ccursor,
|
||||||
|
|
@ -47,10 +58,19 @@ impl CursorRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True if the selected range contains no characters.
|
/// True if the selected range contains no characters.
|
||||||
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.primary.ccursor == self.secondary.ccursor
|
self.primary.ccursor == self.secondary.ccursor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Is `self` a super-set of the the other range?
|
||||||
|
pub fn contains(&self, other: &Self) -> bool {
|
||||||
|
let [self_min, self_max] = self.sorted_cursors();
|
||||||
|
let [other_min, other_max] = other.sorted_cursors();
|
||||||
|
self_min.ccursor.index <= other_min.ccursor.index
|
||||||
|
&& other_max.ccursor.index <= self_max.ccursor.index
|
||||||
|
}
|
||||||
|
|
||||||
/// If there is a selection, None is returned.
|
/// If there is a selection, None is returned.
|
||||||
/// If the two ends is the same, that is returned.
|
/// If the two ends is the same, that is returned.
|
||||||
pub fn single(&self) -> Option<Cursor> {
|
pub fn single(&self) -> Option<Cursor> {
|
||||||
|
|
@ -86,6 +106,93 @@ impl CursorRange {
|
||||||
[self.secondary, self.primary]
|
[self.secondary, self.primary]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn slice_str<'s>(&self, text: &'s str) -> &'s str {
|
||||||
|
let [min, max] = self.sorted_cursors();
|
||||||
|
slice_char_range(text, min.ccursor.index..max.ccursor.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for key presses that are moving the cursor.
|
||||||
|
///
|
||||||
|
/// Returns `true` if we did mutate `self`.
|
||||||
|
pub fn on_key_press(&mut self, galley: &Galley, modifiers: &Modifiers, key: Key) -> bool {
|
||||||
|
match key {
|
||||||
|
Key::A if modifiers.command => {
|
||||||
|
*self = Self::select_all(galley);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::ArrowLeft | Key::ArrowRight if modifiers.is_none() && !self.is_empty() => {
|
||||||
|
if key == Key::ArrowLeft {
|
||||||
|
*self = Self::one(self.sorted_cursors()[0]);
|
||||||
|
} else {
|
||||||
|
*self = Self::one(self.sorted_cursors()[1]);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::ArrowLeft
|
||||||
|
| Key::ArrowRight
|
||||||
|
| Key::ArrowUp
|
||||||
|
| Key::ArrowDown
|
||||||
|
| Key::Home
|
||||||
|
| Key::End => {
|
||||||
|
move_single_cursor(&mut self.primary, galley, key, modifiers);
|
||||||
|
if !modifiers.shift {
|
||||||
|
self.secondary = self.primary;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::P | Key::N | Key::B | Key::F | Key::A | Key::E
|
||||||
|
if cfg!(target_os = "macos") && modifiers.ctrl && !modifiers.shift =>
|
||||||
|
{
|
||||||
|
move_single_cursor(&mut self.primary, galley, key, modifiers);
|
||||||
|
self.secondary = self.primary;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for events that modify the cursor range.
|
||||||
|
///
|
||||||
|
/// Returns `true` if such an event was found and handled.
|
||||||
|
pub fn on_event(&mut self, event: &Event, galley: &Galley, _widget_id: Id) -> bool {
|
||||||
|
match event {
|
||||||
|
Event::Key {
|
||||||
|
modifiers,
|
||||||
|
key,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
} => self.on_key_press(galley, modifiers, *key),
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
Event::AccessKitActionRequest(accesskit::ActionRequest {
|
||||||
|
action: accesskit::Action::SetTextSelection,
|
||||||
|
target,
|
||||||
|
data: Some(accesskit::ActionData::SetTextSelection(selection)),
|
||||||
|
}) => {
|
||||||
|
if _widget_id.accesskit_id() == *target {
|
||||||
|
let primary =
|
||||||
|
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.focus);
|
||||||
|
let secondary =
|
||||||
|
ccursor_from_accesskit_text_position(_widget_id, galley, &selection.anchor);
|
||||||
|
if let (Some(primary), Some(secondary)) = (primary, secondary) {
|
||||||
|
*self = Self {
|
||||||
|
primary: galley.from_ccursor(primary),
|
||||||
|
secondary: galley.from_ccursor(secondary),
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A selected text range (could be a range of length zero).
|
/// A selected text range (could be a range of length zero).
|
||||||
|
|
@ -106,6 +213,7 @@ pub struct CCursorRange {
|
||||||
|
|
||||||
impl CCursorRange {
|
impl CCursorRange {
|
||||||
/// The empty range.
|
/// The empty range.
|
||||||
|
#[inline]
|
||||||
pub fn one(ccursor: CCursor) -> Self {
|
pub fn one(ccursor: CCursor) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primary: ccursor,
|
primary: ccursor,
|
||||||
|
|
@ -113,6 +221,7 @@ impl CCursorRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn two(min: CCursor, max: CCursor) -> Self {
|
pub fn two(min: CCursor, max: CCursor) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primary: max,
|
primary: max,
|
||||||
|
|
@ -120,6 +229,7 @@ impl CCursorRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn is_sorted(&self) -> bool {
|
pub fn is_sorted(&self) -> bool {
|
||||||
let p = self.primary;
|
let p = self.primary;
|
||||||
let s = self.secondary;
|
let s = self.secondary;
|
||||||
|
|
@ -127,6 +237,7 @@ impl CCursorRange {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns the two ends ordered
|
/// returns the two ends ordered
|
||||||
|
#[inline]
|
||||||
pub fn sorted(&self) -> [CCursor; 2] {
|
pub fn sorted(&self) -> [CCursor; 2] {
|
||||||
if self.is_sorted() {
|
if self.is_sorted() {
|
||||||
[self.primary, self.secondary]
|
[self.primary, self.secondary]
|
||||||
|
|
@ -148,3 +259,102 @@ pub struct PCursorRange {
|
||||||
/// This part of the cursor does not move when shift is down.
|
/// This part of the cursor does not move when shift is down.
|
||||||
pub secondary: PCursor,
|
pub secondary: PCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
fn ccursor_from_accesskit_text_position(
|
||||||
|
id: Id,
|
||||||
|
galley: &Galley,
|
||||||
|
position: &accesskit::TextPosition,
|
||||||
|
) -> Option<CCursor> {
|
||||||
|
let mut total_length = 0usize;
|
||||||
|
for (i, row) in galley.rows.iter().enumerate() {
|
||||||
|
let row_id = id.with(i);
|
||||||
|
if row_id.accesskit_id() == position.node {
|
||||||
|
return Some(CCursor {
|
||||||
|
index: total_length + position.character_index,
|
||||||
|
prefer_next_row: !(position.character_index == row.glyphs.len()
|
||||||
|
&& !row.ends_with_newline
|
||||||
|
&& (i + 1) < galley.rows.len()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
total_length += row.glyphs.len() + (row.ends_with_newline as usize);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Move a text cursor based on keyboard
|
||||||
|
fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) {
|
||||||
|
if cfg!(target_os = "macos") && modifiers.ctrl && !modifiers.shift {
|
||||||
|
match key {
|
||||||
|
Key::A => *cursor = galley.cursor_begin_of_row(cursor),
|
||||||
|
Key::E => *cursor = galley.cursor_end_of_row(cursor),
|
||||||
|
Key::P => *cursor = galley.cursor_up_one_row(cursor),
|
||||||
|
Key::N => *cursor = galley.cursor_down_one_row(cursor),
|
||||||
|
Key::B => *cursor = galley.cursor_left_one_character(cursor),
|
||||||
|
Key::F => *cursor = galley.cursor_right_one_character(cursor),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match key {
|
||||||
|
Key::ArrowLeft => {
|
||||||
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
*cursor = galley.from_ccursor(ccursor_previous_word(galley, cursor.ccursor));
|
||||||
|
} else if modifiers.mac_cmd {
|
||||||
|
*cursor = galley.cursor_begin_of_row(cursor);
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_left_one_character(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowRight => {
|
||||||
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
*cursor = galley.from_ccursor(ccursor_next_word(galley, cursor.ccursor));
|
||||||
|
} else if modifiers.mac_cmd {
|
||||||
|
*cursor = galley.cursor_end_of_row(cursor);
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_right_one_character(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowUp => {
|
||||||
|
if modifiers.command {
|
||||||
|
// mac and windows behavior
|
||||||
|
*cursor = Cursor::default();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_up_one_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowDown => {
|
||||||
|
if modifiers.command {
|
||||||
|
// mac and windows behavior
|
||||||
|
*cursor = galley.end();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_down_one_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Home => {
|
||||||
|
if modifiers.ctrl {
|
||||||
|
// windows behavior
|
||||||
|
*cursor = Cursor::default();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_begin_of_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::End => {
|
||||||
|
if modifiers.ctrl {
|
||||||
|
// windows behavior
|
||||||
|
*cursor = galley.end();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_end_of_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
mod builder;
|
mod builder;
|
||||||
|
pub mod cursor_interaction;
|
||||||
mod cursor_range;
|
mod cursor_range;
|
||||||
mod output;
|
mod output;
|
||||||
mod state;
|
mod state;
|
||||||
mod text_buffer;
|
mod text_buffer;
|
||||||
|
|
||||||
|
#[cfg(feature = "accesskit")]
|
||||||
|
pub mod accesskit_text;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
builder::TextEdit, cursor_range::*, output::TextEditOutput, state::TextEditState,
|
builder::{paint_cursor_selection, TextEdit},
|
||||||
|
cursor_range::*,
|
||||||
|
output::TextEditOutput,
|
||||||
|
state::TextCursorState,
|
||||||
|
state::TextEditState,
|
||||||
text_buffer::TextBuffer,
|
text_buffer::TextBuffer,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub struct TextEditOutput {
|
||||||
pub galley: Arc<crate::Galley>,
|
pub galley: Arc<crate::Galley>,
|
||||||
|
|
||||||
/// Where the text in [`Self::galley`] ended up on the screen.
|
/// Where the text in [`Self::galley`] ended up on the screen.
|
||||||
pub text_draw_pos: crate::Pos2,
|
pub galley_pos: crate::Pos2,
|
||||||
|
|
||||||
/// The text was clipped to this rectangle when painted.
|
/// The text was clipped to this rectangle when painted.
|
||||||
pub text_clip_rect: crate::Rect,
|
pub text_clip_rect: crate::Rect,
|
||||||
|
|
@ -21,4 +21,11 @@ pub struct TextEditOutput {
|
||||||
pub cursor_range: Option<super::CursorRange>,
|
pub cursor_range: Option<super::CursorRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TextEditOutput {
|
||||||
|
#[deprecated = "Renamed `self.galley_pos`"]
|
||||||
|
pub fn text_draw_pos(&self) -> crate::Pos2 {
|
||||||
|
self.galley_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(emilk): add `output.paint` and `output.store` and split out that code from `TextEdit::show`.
|
// TODO(emilk): add `output.paint` and `output.store` and split out that code from `TextEdit::show`.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,68 @@ use super::{CCursorRange, CursorRange};
|
||||||
|
|
||||||
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
|
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
|
||||||
|
|
||||||
|
/// The state of a text cursor selection.
|
||||||
|
///
|
||||||
|
/// Used for [`crate::TextEdit`] and [`crate::Label`].
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
|
pub struct TextCursorState {
|
||||||
|
cursor_range: Option<CursorRange>,
|
||||||
|
|
||||||
|
/// This is what is easiest to work with when editing text,
|
||||||
|
/// so users are more likely to read/write this.
|
||||||
|
ccursor_range: Option<CCursorRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextCursorState {
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.cursor_range.is_none() && self.ccursor_range.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The the currently selected range of characters.
|
||||||
|
pub fn char_range(&self) -> Option<CCursorRange> {
|
||||||
|
self.ccursor_range.or_else(|| {
|
||||||
|
self.cursor_range
|
||||||
|
.map(|cursor_range| cursor_range.as_ccursor_range())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range(&mut self, galley: &Galley) -> Option<CursorRange> {
|
||||||
|
self.cursor_range
|
||||||
|
.map(|cursor_range| {
|
||||||
|
// We only use the PCursor (paragraph number, and character offset within that paragraph).
|
||||||
|
// This is so that if we resize the [`TextEdit`] region, and text wrapping changes,
|
||||||
|
// we keep the same byte character offset from the beginning of the text,
|
||||||
|
// even though the number of rows changes
|
||||||
|
// (each paragraph can be several rows, due to word wrapping).
|
||||||
|
// The column (character offset) should be able to extend beyond the last word so that we can
|
||||||
|
// go down and still end up on the same column when we return.
|
||||||
|
CursorRange {
|
||||||
|
primary: galley.from_pcursor(cursor_range.primary.pcursor),
|
||||||
|
secondary: galley.from_pcursor(cursor_range.secondary.pcursor),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
self.ccursor_range.map(|ccursor_range| CursorRange {
|
||||||
|
primary: galley.from_ccursor(ccursor_range.primary),
|
||||||
|
secondary: galley.from_ccursor(ccursor_range.secondary),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the currently selected range of characters.
|
||||||
|
pub fn set_char_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
||||||
|
self.cursor_range = None;
|
||||||
|
self.ccursor_range = ccursor_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_range(&mut self, cursor_range: Option<CursorRange>) {
|
||||||
|
self.cursor_range = cursor_range;
|
||||||
|
self.ccursor_range = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The text edit state stored between frames.
|
/// The text edit state stored between frames.
|
||||||
///
|
///
|
||||||
/// Attention: You also need to `store` the updated state.
|
/// Attention: You also need to `store` the updated state.
|
||||||
|
|
@ -34,11 +96,8 @@ pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub struct TextEditState {
|
pub struct TextEditState {
|
||||||
cursor_range: Option<CursorRange>,
|
/// Controls the text selection.
|
||||||
|
pub cursor: TextCursorState,
|
||||||
/// This is what is easiest to work with when editing text,
|
|
||||||
/// so users are more likely to read/write this.
|
|
||||||
ccursor_range: Option<CCursorRange>,
|
|
||||||
|
|
||||||
/// Wrapped in Arc for cheaper clones.
|
/// Wrapped in Arc for cheaper clones.
|
||||||
#[cfg_attr(feature = "serde", serde(skip))]
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
|
@ -63,22 +122,20 @@ impl TextEditState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The the currently selected range of characters.
|
/// The the currently selected range of characters.
|
||||||
|
#[deprecated = "Use `self.cursor.char_range` instead"]
|
||||||
pub fn ccursor_range(&self) -> Option<CCursorRange> {
|
pub fn ccursor_range(&self) -> Option<CCursorRange> {
|
||||||
self.ccursor_range.or_else(|| {
|
self.cursor.char_range()
|
||||||
self.cursor_range
|
|
||||||
.map(|cursor_range| cursor_range.as_ccursor_range())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the currently selected range of characters.
|
/// Sets the currently selected range of characters.
|
||||||
|
#[deprecated = "Use `self.cursor.set_char_range` instead"]
|
||||||
pub fn set_ccursor_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
pub fn set_ccursor_range(&mut self, ccursor_range: Option<CCursorRange>) {
|
||||||
self.cursor_range = None;
|
self.cursor.set_char_range(ccursor_range);
|
||||||
self.ccursor_range = ccursor_range;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated = "Use `self.cursor.set_range` instead"]
|
||||||
pub fn set_cursor_range(&mut self, cursor_range: Option<CursorRange>) {
|
pub fn set_cursor_range(&mut self, cursor_range: Option<CursorRange>) {
|
||||||
self.cursor_range = cursor_range;
|
self.cursor.set_range(cursor_range);
|
||||||
self.ccursor_range = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undoer(&self) -> TextEditUndoer {
|
pub fn undoer(&self) -> TextEditUndoer {
|
||||||
|
|
@ -93,26 +150,8 @@ impl TextEditState {
|
||||||
self.set_undoer(TextEditUndoer::default());
|
self.set_undoer(TextEditUndoer::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated = "Use `self.cursor.range` instead"]
|
||||||
pub fn cursor_range(&mut self, galley: &Galley) -> Option<CursorRange> {
|
pub fn cursor_range(&mut self, galley: &Galley) -> Option<CursorRange> {
|
||||||
self.cursor_range
|
self.cursor.range(galley)
|
||||||
.map(|cursor_range| {
|
|
||||||
// We only use the PCursor (paragraph number, and character offset within that paragraph).
|
|
||||||
// This is so that if we resize the [`TextEdit`] region, and text wrapping changes,
|
|
||||||
// we keep the same byte character offset from the beginning of the text,
|
|
||||||
// even though the number of rows changes
|
|
||||||
// (each paragraph can be several rows, due to word wrapping).
|
|
||||||
// The column (character offset) should be able to extend beyond the last word so that we can
|
|
||||||
// go down and still end up on the same column when we return.
|
|
||||||
CursorRange {
|
|
||||||
primary: galley.from_pcursor(cursor_range.primary.pcursor),
|
|
||||||
secondary: galley.from_pcursor(cursor_range.secondary.pcursor),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
self.ccursor_range.map(|ccursor_range| CursorRange {
|
|
||||||
primary: galley.from_ccursor(ccursor_range.primary),
|
|
||||||
secondary: galley.from_ccursor(ccursor_range.secondary),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use std::{borrow::Cow, ops::Range};
|
use std::{borrow::Cow, ops::Range};
|
||||||
|
|
||||||
|
use super::cursor_interaction::{byte_index_from_char_index, slice_char_range};
|
||||||
|
|
||||||
/// Trait constraining what types [`crate::TextEdit`] may use as
|
/// Trait constraining what types [`crate::TextEdit`] may use as
|
||||||
/// an underlying buffer.
|
/// an underlying buffer.
|
||||||
///
|
///
|
||||||
|
|
@ -13,10 +15,7 @@ pub trait TextBuffer {
|
||||||
|
|
||||||
/// Reads the given character range.
|
/// Reads the given character range.
|
||||||
fn char_range(&self, char_range: Range<usize>) -> &str {
|
fn char_range(&self, char_range: Range<usize>) -> &str {
|
||||||
assert!(char_range.start <= char_range.end);
|
slice_char_range(self.as_str(), char_range)
|
||||||
let start_byte = self.byte_index_from_char_index(char_range.start);
|
|
||||||
let end_byte = self.byte_index_from_char_index(char_range.end);
|
|
||||||
&self.as_str()[start_byte..end_byte]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn byte_index_from_char_index(&self, char_index: usize) -> usize {
|
fn byte_index_from_char_index(&self, char_index: usize) -> usize {
|
||||||
|
|
@ -68,7 +67,7 @@ impl TextBuffer for String {
|
||||||
|
|
||||||
fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
|
fn insert_text(&mut self, text: &str, char_index: usize) -> usize {
|
||||||
// Get the byte index from the character index
|
// Get the byte index from the character index
|
||||||
let byte_idx = self.byte_index_from_char_index(char_index);
|
let byte_idx = byte_index_from_char_index(self.as_str(), char_index);
|
||||||
|
|
||||||
// Then insert the string
|
// Then insert the string
|
||||||
self.insert_str(byte_idx, text);
|
self.insert_str(byte_idx, text);
|
||||||
|
|
@ -80,8 +79,8 @@ impl TextBuffer for String {
|
||||||
assert!(char_range.start <= char_range.end);
|
assert!(char_range.start <= char_range.end);
|
||||||
|
|
||||||
// Get both byte indices
|
// Get both byte indices
|
||||||
let byte_start = self.byte_index_from_char_index(char_range.start);
|
let byte_start = byte_index_from_char_index(self.as_str(), char_range.start);
|
||||||
let byte_end = self.byte_index_from_char_index(char_range.end);
|
let byte_end = byte_index_from_char_index(self.as_str(), char_range.end);
|
||||||
|
|
||||||
// Then drain all characters within this range
|
// Then drain all characters within this range
|
||||||
self.drain(byte_start..byte_end);
|
self.drain(byte_start..byte_end);
|
||||||
|
|
@ -146,12 +145,3 @@ impl<'a> TextBuffer for &'a str {
|
||||||
|
|
||||||
fn delete_char_range(&mut self, _ch_range: Range<usize>) {}
|
fn delete_char_range(&mut self, _ch_range: Range<usize>) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
|
|
||||||
for (ci, (bi, _)) in s.char_indices().enumerate() {
|
|
||||||
if ci == char_index {
|
|
||||||
return bi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.len()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -205,21 +205,13 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
|
||||||
} else if let Some(colored_text) = colored_text {
|
} else if let Some(colored_text) = colored_text {
|
||||||
colored_text.ui(ui);
|
colored_text.ui(ui);
|
||||||
} else if let Some(text) = &text {
|
} else if let Some(text) = &text {
|
||||||
selectable_text(ui, text);
|
ui.add(egui::Label::new(text).selectable(true));
|
||||||
} else {
|
} else {
|
||||||
ui.monospace("[binary]");
|
ui.monospace("[binary]");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selectable_text(ui: &mut egui::Ui, mut text: &str) {
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::multiline(&mut text)
|
|
||||||
.desired_width(f32::INFINITY)
|
|
||||||
.font(egui::TextStyle::Monospace),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Syntax highlighting:
|
// Syntax highlighting:
|
||||||
|
|
||||||
|
|
@ -240,31 +232,9 @@ struct ColoredText(egui::text::LayoutJob);
|
||||||
|
|
||||||
impl ColoredText {
|
impl ColoredText {
|
||||||
pub fn ui(&self, ui: &mut egui::Ui) {
|
pub fn ui(&self, ui: &mut egui::Ui) {
|
||||||
if true {
|
let mut job = self.0.clone();
|
||||||
// Selectable text:
|
job.wrap.max_width = ui.available_width();
|
||||||
let mut layouter = |ui: &egui::Ui, _string: &str, wrap_width: f32| {
|
let galley = ui.fonts(|f| f.layout_job(job));
|
||||||
let mut layout_job = self.0.clone();
|
ui.add(egui::Label::new(galley).selectable(true));
|
||||||
layout_job.wrap.max_width = wrap_width;
|
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut text = self.0.text.as_str();
|
|
||||||
ui.add(
|
|
||||||
egui::TextEdit::multiline(&mut text)
|
|
||||||
.font(egui::TextStyle::Monospace)
|
|
||||||
.desired_width(f32::INFINITY)
|
|
||||||
.layouter(&mut layouter),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let mut job = self.0.clone();
|
|
||||||
job.wrap.max_width = ui.available_width();
|
|
||||||
let galley = ui.fonts(|f| f.layout_job(job));
|
|
||||||
let (response, painter) = ui.allocate_painter(galley.size(), egui::Sense::hover());
|
|
||||||
painter.add(egui::Shape::galley(
|
|
||||||
response.rect.min,
|
|
||||||
galley,
|
|
||||||
ui.visuals().text_color(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,7 @@ impl super::View for TextEditDemo {
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
ui.label("Selected text: ");
|
ui.label("Selected text: ");
|
||||||
if let Some(text_cursor_range) = output.cursor_range {
|
if let Some(text_cursor_range) = output.cursor_range {
|
||||||
use egui::TextBuffer as _;
|
let selected_text = text_cursor_range.slice_str(text);
|
||||||
let selected_chars = text_cursor_range.as_sorted_char_range();
|
|
||||||
let selected_text = text.char_range(selected_chars);
|
|
||||||
ui.code(selected_text);
|
ui.code(selected_text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -92,7 +90,9 @@ impl super::View for TextEditDemo {
|
||||||
let text_edit_id = output.response.id;
|
let text_edit_id = output.response.id;
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
let ccursor = egui::text::CCursor::new(0);
|
let ccursor = egui::text::CCursor::new(0);
|
||||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
state
|
||||||
|
.cursor
|
||||||
|
.set_char_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||||
state.store(ui.ctx(), text_edit_id);
|
state.store(ui.ctx(), text_edit_id);
|
||||||
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
|
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,9 @@ impl super::View for TextEditDemo {
|
||||||
let text_edit_id = output.response.id;
|
let text_edit_id = output.response.id;
|
||||||
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), text_edit_id) {
|
||||||
let ccursor = egui::text::CCursor::new(text.chars().count());
|
let ccursor = egui::text::CCursor::new(text.chars().count());
|
||||||
state.set_ccursor_range(Some(egui::text::CCursorRange::one(ccursor)));
|
state
|
||||||
|
.cursor
|
||||||
|
.set_char_range(Some(egui::text::CCursorRange::one(ccursor)));
|
||||||
state.store(ui.ctx(), text_edit_id);
|
state.store(ui.ctx(), text_edit_id);
|
||||||
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
|
ui.ctx().memory_mut(|mem| mem.request_focus(text_edit_id)); // give focus back to the [`TextEdit`].
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,10 +95,10 @@ impl EasyMarkEditor {
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
if let Some(mut state) = TextEdit::load_state(ui.ctx(), response.id) {
|
||||||
if let Some(mut ccursor_range) = state.ccursor_range() {
|
if let Some(mut ccursor_range) = state.cursor.char_range() {
|
||||||
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
let any_change = shortcuts(ui, code, &mut ccursor_range);
|
||||||
if any_change {
|
if any_change {
|
||||||
state.set_ccursor_range(Some(ccursor_range));
|
state.cursor.set_char_range(Some(ccursor_range));
|
||||||
state.store(ui.ctx(), response.id);
|
state.store(ui.ctx(), response.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ impl LayoutJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.sections.is_empty()
|
self.sections.is_empty()
|
||||||
}
|
}
|
||||||
|
|
@ -618,22 +618,45 @@ impl Row {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Galley {
|
impl Galley {
|
||||||
#[inline(always)]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.job.is_empty()
|
self.job.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The full, non-elided text of the input job.
|
/// The full, non-elided text of the input job.
|
||||||
#[inline(always)]
|
#[inline]
|
||||||
pub fn text(&self) -> &str {
|
pub fn text(&self) -> &str {
|
||||||
&self.job.text
|
&self.job.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn size(&self) -> Vec2 {
|
pub fn size(&self) -> Vec2 {
|
||||||
self.rect.size()
|
self.rect.size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for Galley {
|
||||||
|
#[inline]
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
self.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::borrow::Borrow<str> for Galley {
|
||||||
|
#[inline]
|
||||||
|
fn borrow(&self) -> &str {
|
||||||
|
self.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Deref for Galley {
|
||||||
|
type Target = str;
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &str {
|
||||||
|
self.text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// ## Physical positions
|
/// ## Physical positions
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ set -x
|
||||||
# Basically does what the CI does.
|
# Basically does what the CI does.
|
||||||
|
|
||||||
cargo install --quiet cargo-cranky # Uses lints defined in Cranky.toml. See https://github.com/ericseppanen/cargo-cranky
|
cargo install --quiet cargo-cranky # Uses lints defined in Cranky.toml. See https://github.com/ericseppanen/cargo-cranky
|
||||||
cargo install --quiet typos-cli
|
cargo +1.75.0 install --quiet typos-cli
|
||||||
|
|
||||||
# web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,
|
# web_sys_unstable_apis is required to enable the web_sys clipboard API which eframe web uses,
|
||||||
# as well as by the wasm32-backend of the wgpu crate.
|
# as well as by the wasm32-backend of the wgpu crate.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue