diff --git a/egui/src/widgets/text_edit/builder.rs b/egui/src/widgets/text_edit/builder.rs index 8c6812e1..95d911ab 100644 --- a/egui/src/widgets/text_edit/builder.rs +++ b/egui/src/widgets/text_edit/builder.rs @@ -44,6 +44,8 @@ use super::{CCursorRange, CursorRange, TextEditOutput, TextEditState}; /// } /// ``` /// +/// ## Advanced usage +/// See [`TextEdit::show`]. #[must_use = "You should put this widget in an ui with `ui.add(widget);`"] pub struct TextEdit<'t> { text: &'t mut dyn TextBuffer, @@ -242,6 +244,20 @@ impl<'t> Widget for TextEdit<'t> { impl<'t> TextEdit<'t> { /// Show the [`TextEdit`], returning a rich [`TextEditOutput`]. + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// # let mut my_string = String::new(); + /// let output = egui::TextEdit::singleline(&mut my_string).show(ui); + /// if let Some(text_cursor_range) = output.cursor_range { + /// use egui::TextBuffer as _; + /// let selected_chars = text_cursor_range.as_sorted_char_range(); + /// let selected_text = my_string.char_range(selected_chars); + /// ui.label("Selected text: "); + /// ui.monospace(selected_text); + /// } + /// # }); + /// ``` pub fn show(self, ui: &mut Ui) -> TextEditOutput { let is_mutable = self.text.is_mutable(); let frame = self.frame; @@ -386,7 +402,8 @@ impl<'t> TextEdit<'t> { Sense::hover() }; let mut response = ui.interact(rect, id, sense); - let painter = ui.painter_at(rect); + let text_clip_rect = rect; + let painter = ui.painter_at(text_clip_rect); if interactive { if let Some(pointer_pos) = ui.input().pointer.interact_pos() { @@ -593,6 +610,8 @@ impl<'t> TextEdit<'t> { TextEditOutput { response, galley, + text_draw_pos, + text_clip_rect, state, cursor_range, } @@ -806,7 +825,7 @@ fn paint_cursor_selection( // 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 [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); let min = min.rcursor; let max = max.rcursor; @@ -875,7 +894,7 @@ fn paint_cursor_end( // ---------------------------------------------------------------------------- fn selected_str<'s>(text: &'s dyn TextBuffer, cursor_range: &CursorRange) -> &'s str { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); text.char_range(min.ccursor.index..max.ccursor.index) } @@ -886,7 +905,7 @@ fn insert_text(ccursor: &mut CCursor, text: &mut dyn TextBuffer, text_to_insert: // ---------------------------------------------------------------------------- fn delete_selected(text: &mut dyn TextBuffer, cursor_range: &CursorRange) -> CCursor { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); delete_selected_ccursor_range(text, [min.ccursor, max.ccursor]) } @@ -927,7 +946,7 @@ fn delete_paragraph_before_cursor( galley: &Galley, cursor_range: &CursorRange, ) -> CCursor { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); let min = galley.from_pcursor(PCursor { paragraph: min.pcursor.paragraph, offset: 0, @@ -945,7 +964,7 @@ fn delete_paragraph_after_cursor( galley: &Galley, cursor_range: &CursorRange, ) -> CCursor { - let [min, max] = cursor_range.sorted(); + let [min, max] = cursor_range.sorted_cursors(); let max = galley.from_pcursor(PCursor { paragraph: max.pcursor.paragraph, offset: usize::MAX, // end of paragraph @@ -984,7 +1003,7 @@ fn on_key_press( }; Some(CCursorRange::one(ccursor)) } - Key::Delete if !(cfg!(target_os = "windows") && modifiers.shift) => { + Key::Delete if !modifiers.shift || !cfg!(target_os = "windows") => { let ccursor = if modifiers.mac_cmd { delete_paragraph_after_cursor(text, galley, cursor_range) } else if let Some(cursor) = cursor_range.single() { @@ -1031,9 +1050,9 @@ fn on_key_press( Key::ArrowLeft | Key::ArrowRight if modifiers.is_none() && !cursor_range.is_empty() => { if key == Key::ArrowLeft { - *cursor_range = CursorRange::one(cursor_range.sorted()[0]); + *cursor_range = CursorRange::one(cursor_range.sorted_cursors()[0]); } else { - *cursor_range = CursorRange::one(cursor_range.sorted()[1]); + *cursor_range = CursorRange::one(cursor_range.sorted_cursors()[1]); } None } diff --git a/egui/src/widgets/text_edit/cursor_range.rs b/egui/src/widgets/text_edit/cursor_range.rs index 938de28a..360260b8 100644 --- a/egui/src/widgets/text_edit/cursor_range.rs +++ b/egui/src/widgets/text_edit/cursor_range.rs @@ -37,6 +37,15 @@ impl CursorRange { } } + /// The range of selected character indices. + pub fn as_sorted_char_range(&self) -> std::ops::Range { + let [start, end] = self.sorted_cursors(); + std::ops::Range { + start: start.ccursor.index, + end: end.ccursor.index, + } + } + /// True if the selected range contains no characters. pub fn is_empty(&self) -> bool { self.primary.ccursor == self.secondary.ccursor @@ -58,8 +67,19 @@ impl CursorRange { (p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row) } + pub fn sorted(self) -> Self { + if self.is_sorted() { + self + } else { + Self { + primary: self.secondary, + secondary: self.primary, + } + } + } + /// returns the two ends ordered - pub fn sorted(&self) -> [Cursor; 2] { + pub fn sorted_cursors(&self) -> [Cursor; 2] { if self.is_sorted() { [self.primary, self.secondary] } else { diff --git a/egui/src/widgets/text_edit/output.rs b/egui/src/widgets/text_edit/output.rs index fecc4c71..0464d646 100644 --- a/egui/src/widgets/text_edit/output.rs +++ b/egui/src/widgets/text_edit/output.rs @@ -8,9 +8,17 @@ pub struct TextEditOutput { /// How the text was displayed. pub galley: Arc, - /// The state we stored after the run/ + /// Where the text in [`Self::galley`] ended up on the screen. + pub text_draw_pos: crate::Pos2, + + /// The text was clipped to this rectangle when painted. + pub text_clip_rect: crate::Rect, + + /// The state we stored after the run. pub state: super::TextEditState, /// Where the text cursor is. pub cursor_range: Option, } + +// TODO: add `output.paint` and `output.store` and split out that code from `TextEdit::show`. diff --git a/egui_demo_lib/src/apps/demo/demo_app_windows.rs b/egui_demo_lib/src/apps/demo/demo_app_windows.rs index 5db67ebd..2285b551 100644 --- a/egui_demo_lib/src/apps/demo/demo_app_windows.rs +++ b/egui_demo_lib/src/apps/demo/demo_app_windows.rs @@ -28,6 +28,7 @@ impl Default for Demos { Box::new(super::plot_demo::PlotDemo::default()), Box::new(super::scrolling::Scrolling::default()), Box::new(super::sliders::Sliders::default()), + Box::new(super::text_edit::TextEdit::default()), Box::new(super::widget_gallery::WidgetGallery::default()), Box::new(super::window_options::WindowOptions::default()), Box::new(super::tests::WindowResizeTest::default()), diff --git a/egui_demo_lib/src/apps/demo/mod.rs b/egui_demo_lib/src/apps/demo/mod.rs index e8f1a688..22b23284 100644 --- a/egui_demo_lib/src/apps/demo/mod.rs +++ b/egui_demo_lib/src/apps/demo/mod.rs @@ -21,6 +21,7 @@ pub mod plot_demo; pub mod scrolling; pub mod sliders; pub mod tests; +pub mod text_edit; pub mod toggle_switch; pub mod widget_gallery; pub mod window_options; diff --git a/egui_demo_lib/src/apps/demo/text_edit.rs b/egui_demo_lib/src/apps/demo/text_edit.rs new file mode 100644 index 00000000..1d2294de --- /dev/null +++ b/egui_demo_lib/src/apps/demo/text_edit.rs @@ -0,0 +1,83 @@ +/// Showcase [`TextEdit`]. +#[derive(PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr(feature = "serde", serde(default))] +pub struct TextEdit { + pub text: String, +} + +impl Default for TextEdit { + fn default() -> Self { + Self { + text: "Edit this text".to_owned(), + } + } +} + +impl super::Demo for TextEdit { + fn name(&self) -> &'static str { + "🖹 TextEdit" + } + + fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .show(ctx, |ui| { + use super::View as _; + self.ui(ui); + }); + } +} + +impl super::View for TextEdit { + fn ui(&mut self, ui: &mut egui::Ui) { + let Self { text } = self; + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Advanced usage of "); + ui.code("TextEdit"); + ui.label("."); + }); + + let output = egui::TextEdit::multiline(text) + .hint_text("Type something!") + .show(ui); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Selected text: "); + if let Some(text_cursor_range) = output.cursor_range { + use egui::TextBuffer as _; + let selected_chars = text_cursor_range.as_sorted_char_range(); + let selected_text = text.char_range(selected_chars); + ui.code(selected_text); + } + }); + + let anything_selected = output + .cursor_range + .map_or(false, |cursor| !cursor.is_empty()); + + ui.add_enabled( + anything_selected, + egui::Label::new("Press ctrl+T to toggle the case of selected text (cmd+T on Mac)"), + ); + if ui.input().modifiers.command_only() && ui.input().key_pressed(egui::Key::T) { + if let Some(text_cursor_range) = output.cursor_range { + use egui::TextBuffer as _; + let selected_chars = text_cursor_range.as_sorted_char_range(); + let selected_text = text.char_range(selected_chars.clone()); + let upper_case = selected_text.to_uppercase(); + let new_text = if selected_text == upper_case { + selected_text.to_lowercase() + } else { + upper_case + }; + text.delete_char_range(selected_chars.clone()); + text.insert_text(&new_text, selected_chars.start); + } + } + } +}