Add demo of advanced TextEdit usage
This commit is contained in:
parent
d31f7d6522
commit
7863f44111
6 changed files with 143 additions and 11 deletions
|
@ -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);`"]
|
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
|
||||||
pub struct TextEdit<'t> {
|
pub struct TextEdit<'t> {
|
||||||
text: &'t mut dyn TextBuffer,
|
text: &'t mut dyn TextBuffer,
|
||||||
|
@ -242,6 +244,20 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
|
|
||||||
impl<'t> TextEdit<'t> {
|
impl<'t> TextEdit<'t> {
|
||||||
/// Show the [`TextEdit`], returning a rich [`TextEditOutput`].
|
/// 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 {
|
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
|
||||||
let is_mutable = self.text.is_mutable();
|
let is_mutable = self.text.is_mutable();
|
||||||
let frame = self.frame;
|
let frame = self.frame;
|
||||||
|
@ -386,7 +402,8 @@ impl<'t> TextEdit<'t> {
|
||||||
Sense::hover()
|
Sense::hover()
|
||||||
};
|
};
|
||||||
let mut response = ui.interact(rect, id, sense);
|
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 interactive {
|
||||||
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
|
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
|
||||||
|
@ -593,6 +610,8 @@ impl<'t> TextEdit<'t> {
|
||||||
TextEditOutput {
|
TextEditOutput {
|
||||||
response,
|
response,
|
||||||
galley,
|
galley,
|
||||||
|
text_draw_pos,
|
||||||
|
text_clip_rect,
|
||||||
state,
|
state,
|
||||||
cursor_range,
|
cursor_range,
|
||||||
}
|
}
|
||||||
|
@ -806,7 +825,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 = 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 min = min.rcursor;
|
||||||
let max = max.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 {
|
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)
|
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 {
|
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])
|
delete_selected_ccursor_range(text, [min.ccursor, max.ccursor])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -927,7 +946,7 @@ fn delete_paragraph_before_cursor(
|
||||||
galley: &Galley,
|
galley: &Galley,
|
||||||
cursor_range: &CursorRange,
|
cursor_range: &CursorRange,
|
||||||
) -> CCursor {
|
) -> CCursor {
|
||||||
let [min, max] = cursor_range.sorted();
|
let [min, max] = cursor_range.sorted_cursors();
|
||||||
let min = galley.from_pcursor(PCursor {
|
let min = galley.from_pcursor(PCursor {
|
||||||
paragraph: min.pcursor.paragraph,
|
paragraph: min.pcursor.paragraph,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
@ -945,7 +964,7 @@ fn delete_paragraph_after_cursor(
|
||||||
galley: &Galley,
|
galley: &Galley,
|
||||||
cursor_range: &CursorRange,
|
cursor_range: &CursorRange,
|
||||||
) -> CCursor {
|
) -> CCursor {
|
||||||
let [min, max] = cursor_range.sorted();
|
let [min, max] = cursor_range.sorted_cursors();
|
||||||
let max = galley.from_pcursor(PCursor {
|
let max = galley.from_pcursor(PCursor {
|
||||||
paragraph: max.pcursor.paragraph,
|
paragraph: max.pcursor.paragraph,
|
||||||
offset: usize::MAX, // end of paragraph
|
offset: usize::MAX, // end of paragraph
|
||||||
|
@ -984,7 +1003,7 @@ fn on_key_press(
|
||||||
};
|
};
|
||||||
Some(CCursorRange::one(ccursor))
|
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 {
|
let ccursor = if modifiers.mac_cmd {
|
||||||
delete_paragraph_after_cursor(text, galley, cursor_range)
|
delete_paragraph_after_cursor(text, galley, cursor_range)
|
||||||
} else if let Some(cursor) = cursor_range.single() {
|
} 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() => {
|
Key::ArrowLeft | Key::ArrowRight if modifiers.is_none() && !cursor_range.is_empty() => {
|
||||||
if key == Key::ArrowLeft {
|
if key == Key::ArrowLeft {
|
||||||
*cursor_range = CursorRange::one(cursor_range.sorted()[0]);
|
*cursor_range = CursorRange::one(cursor_range.sorted_cursors()[0]);
|
||||||
} else {
|
} else {
|
||||||
*cursor_range = CursorRange::one(cursor_range.sorted()[1]);
|
*cursor_range = CursorRange::one(cursor_range.sorted_cursors()[1]);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,15 @@ impl CursorRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The range of selected character indices.
|
||||||
|
pub fn as_sorted_char_range(&self) -> std::ops::Range<usize> {
|
||||||
|
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.
|
/// True if the selected range contains no characters.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.primary.ccursor == self.secondary.ccursor
|
self.primary.ccursor == self.secondary.ccursor
|
||||||
|
@ -58,8 +67,19 @@ impl CursorRange {
|
||||||
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
|
(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
|
/// returns the two ends ordered
|
||||||
pub fn sorted(&self) -> [Cursor; 2] {
|
pub fn sorted_cursors(&self) -> [Cursor; 2] {
|
||||||
if self.is_sorted() {
|
if self.is_sorted() {
|
||||||
[self.primary, self.secondary]
|
[self.primary, self.secondary]
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,9 +8,17 @@ pub struct TextEditOutput {
|
||||||
/// How the text was displayed.
|
/// How the text was displayed.
|
||||||
pub galley: Arc<crate::Galley>,
|
pub galley: Arc<crate::Galley>,
|
||||||
|
|
||||||
/// 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,
|
pub state: super::TextEditState,
|
||||||
|
|
||||||
/// Where the text cursor is.
|
/// Where the text cursor is.
|
||||||
pub cursor_range: Option<super::CursorRange>,
|
pub cursor_range: Option<super::CursorRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: add `output.paint` and `output.store` and split out that code from `TextEdit::show`.
|
||||||
|
|
|
@ -28,6 +28,7 @@ impl Default for Demos {
|
||||||
Box::new(super::plot_demo::PlotDemo::default()),
|
Box::new(super::plot_demo::PlotDemo::default()),
|
||||||
Box::new(super::scrolling::Scrolling::default()),
|
Box::new(super::scrolling::Scrolling::default()),
|
||||||
Box::new(super::sliders::Sliders::default()),
|
Box::new(super::sliders::Sliders::default()),
|
||||||
|
Box::new(super::text_edit::TextEdit::default()),
|
||||||
Box::new(super::widget_gallery::WidgetGallery::default()),
|
Box::new(super::widget_gallery::WidgetGallery::default()),
|
||||||
Box::new(super::window_options::WindowOptions::default()),
|
Box::new(super::window_options::WindowOptions::default()),
|
||||||
Box::new(super::tests::WindowResizeTest::default()),
|
Box::new(super::tests::WindowResizeTest::default()),
|
||||||
|
|
|
@ -21,6 +21,7 @@ pub mod plot_demo;
|
||||||
pub mod scrolling;
|
pub mod scrolling;
|
||||||
pub mod sliders;
|
pub mod sliders;
|
||||||
pub mod tests;
|
pub mod tests;
|
||||||
|
pub mod text_edit;
|
||||||
pub mod toggle_switch;
|
pub mod toggle_switch;
|
||||||
pub mod widget_gallery;
|
pub mod widget_gallery;
|
||||||
pub mod window_options;
|
pub mod window_options;
|
||||||
|
|
83
egui_demo_lib/src/apps/demo/text_edit.rs
Normal file
83
egui_demo_lib/src/apps/demo/text_edit.rs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue