Skip to content

Commit edde2b2

Browse files
committed
Avoid cloning text in TextEdit for Arc<str> text buffers
1 parent bf5604b commit edde2b2

File tree

5 files changed

+68
-31
lines changed

5 files changed

+68
-31
lines changed

crates/egui/src/data/output.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! All the data egui returns to the backend at the end of each frame.
22
3+
use std::sync::Arc;
4+
35
use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
46

57
/// What egui emits each frame from [`crate::Context::run`].
@@ -495,10 +497,10 @@ pub struct WidgetInfo {
495497
pub label: Option<String>,
496498

497499
/// The contents of some editable text (for [`TextEdit`](crate::TextEdit) fields).
498-
pub current_text_value: Option<String>,
500+
pub current_text_value: Option<Arc<str>>,
499501

500502
/// The previous text value.
501-
pub prev_text_value: Option<String>,
503+
pub prev_text_value: Option<Arc<str>>,
502504

503505
/// The current value of checkboxes and radio buttons.
504506
pub selected: Option<bool>,
@@ -618,12 +620,12 @@ impl WidgetInfo {
618620
#[expect(clippy::needless_pass_by_value)]
619621
pub fn text_edit(
620622
enabled: bool,
621-
prev_text_value: impl ToString,
622-
text_value: impl ToString,
623+
prev_text_value: impl Into<Arc<str>>,
624+
text_value: impl Into<Arc<str>>,
623625
hint_text: impl ToString,
624626
) -> Self {
625-
let text_value = text_value.to_string();
626-
let prev_text_value = prev_text_value.to_string();
627+
let text_value = text_value.into();
628+
let prev_text_value = prev_text_value.into();
627629
let hint_text = hint_text.to_string();
628630
let prev_text_value = if text_value == prev_text_value {
629631
None
@@ -643,12 +645,12 @@ impl WidgetInfo {
643645
pub fn text_selection_changed(
644646
enabled: bool,
645647
text_selection: std::ops::RangeInclusive<usize>,
646-
current_text_value: impl ToString,
648+
current_text_value: impl Into<Arc<str>>,
647649
) -> Self {
648650
Self {
649651
enabled,
650652
text_selection: Some(text_selection),
651-
current_text_value: Some(current_text_value.to_string()),
653+
current_text_value: Some(current_text_value.into()),
652654
..Self::new(WidgetType::TextEdit)
653655
}
654656
}

crates/egui/src/response.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -874,7 +874,7 @@ impl Response {
874874
}
875875
}
876876
if let Some(value) = info.current_text_value {
877-
builder.set_value(value);
877+
builder.set_value((*value).to_owned());
878878
}
879879
if let Some(value) = info.value {
880880
builder.set_numeric_value(value);

crates/egui/src/widgets/text_edit/builder.rs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ impl TextEdit<'_> {
500500
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
501501
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
502502

503-
let prev_text = text.as_str().to_owned();
503+
let prev_text = text.clone_to_arc();
504504
let hint_text_str = hint_text.text().to_owned();
505505

506506
let font_id = font_selection.resolve(ui.style());
@@ -516,7 +516,8 @@ impl TextEdit<'_> {
516516

517517
let font_id_clone = font_id.clone();
518518
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
519-
let text = mask_if_password(password, text.as_str());
519+
// TODO: Keep as Arc<str>!
520+
let text = (*mask_if_password(password, text.clone_to_arc())).to_owned();
520521
let layout_job = if multiline {
521522
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
522523
} else {
@@ -819,8 +820,8 @@ impl TextEdit<'_> {
819820
response.widget_info(|| {
820821
WidgetInfo::text_edit(
821822
ui.is_enabled(),
822-
mask_if_password(password, prev_text.as_str()),
823-
mask_if_password(password, text.as_str()),
823+
mask_if_password(password, prev_text.clone()),
824+
mask_if_password(password, text.clone_to_arc()),
824825
hint_text_str.as_str(),
825826
)
826827
});
@@ -830,15 +831,15 @@ impl TextEdit<'_> {
830831
let info = WidgetInfo::text_selection_changed(
831832
ui.is_enabled(),
832833
char_range,
833-
mask_if_password(password, text.as_str()),
834+
mask_if_password(password, text.clone_to_arc()),
834835
);
835836
response.output_event(OutputEvent::TextSelectionChanged(info));
836837
} else {
837838
response.widget_info(|| {
838839
WidgetInfo::text_edit(
839840
ui.is_enabled(),
840-
mask_if_password(password, prev_text.as_str()),
841-
mask_if_password(password, text.as_str()),
841+
mask_if_password(password, prev_text.clone()),
842+
mask_if_password(password, text.clone_to_arc()),
842843
hint_text_str.as_str(),
843844
)
844845
});
@@ -875,7 +876,7 @@ impl TextEdit<'_> {
875876
}
876877
}
877878

878-
fn mask_if_password(is_password: bool, text: &str) -> String {
879+
fn mask_if_password(is_password: bool, text: Arc<str>) -> Arc<str> {
879880
fn mask_password(text: &str) -> String {
880881
std::iter::repeat_n(
881882
epaint::text::PASSWORD_REPLACEMENT_CHAR,
@@ -885,9 +886,9 @@ fn mask_if_password(is_password: bool, text: &str) -> String {
885886
}
886887

887888
if is_password {
888-
mask_password(text)
889+
mask_password(&text).into()
889890
} else {
890-
text.to_owned()
891+
text
891892
}
892893
}
893894

@@ -916,10 +917,10 @@ fn events(
916917

917918
// We feed state to the undoer both before and after handling input
918919
// so that the undoer creates automatic saves even when there are no events for a while.
919-
state.undoer.lock().feed_state(
920-
ui.input(|i| i.time),
921-
&(cursor_range, text.as_str().to_owned()),
922-
);
920+
state
921+
.undoer
922+
.lock()
923+
.feed_state(ui.input(|i| i.time), &(cursor_range, text.clone_to_arc()));
923924

924925
let copy_if_not_password = |ui: &Ui, text: String| {
925926
if !password {
@@ -1030,7 +1031,7 @@ fn events(
10301031
if let Some((redo_ccursor_range, redo_txt)) = state
10311032
.undoer
10321033
.lock()
1033-
.redo(&(cursor_range, text.as_str().to_owned()))
1034+
.redo(&(cursor_range, text.clone_to_arc()))
10341035
{
10351036
text.replace_with(redo_txt);
10361037
Some(*redo_ccursor_range)
@@ -1048,7 +1049,7 @@ fn events(
10481049
if let Some((undo_ccursor_range, undo_txt)) = state
10491050
.undoer
10501051
.lock()
1051-
.undo(&(cursor_range, text.as_str().to_owned()))
1052+
.undo(&(cursor_range, text.clone_to_arc()))
10521053
{
10531054
text.replace_with(undo_txt);
10541055
Some(*undo_ccursor_range)
@@ -1126,10 +1127,10 @@ fn events(
11261127

11271128
state.cursor.set_char_range(Some(cursor_range));
11281129

1129-
state.undoer.lock().feed_state(
1130-
ui.input(|i| i.time),
1131-
&(cursor_range, text.as_str().to_owned()),
1132-
);
1130+
state
1131+
.undoer
1132+
.lock()
1133+
.feed_state(ui.input(|i| i.time), &(cursor_range, text.clone_to_arc()));
11331134

11341135
(any_change, cursor_range)
11351136
}

crates/egui/src/widgets/text_edit/state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
text_selection::{CCursorRange, TextCursorState},
88
};
99

10-
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, String)>;
10+
pub type TextEditUndoer = crate::util::undoer::Undoer<(CCursorRange, Arc<str>)>;
1111

1212
/// The text edit state stored between frames.
1313
///

crates/egui/src/widgets/text_edit/text_buffer.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{borrow::Cow, ops::Range};
1+
use std::{borrow::Cow, ops::Range, sync::Arc};
22

33
use epaint::{
44
Galley,
@@ -24,6 +24,15 @@ pub trait TextBuffer {
2424
/// Returns this buffer as a `str`.
2525
fn as_str(&self) -> &str;
2626

27+
/// Clone this buffer to an [`Arc<str>`].
28+
///
29+
/// By default this is implemented by creating a new [`Arc`] fromm the result
30+
/// of [`as_str`], however if the buffer is already managed by an [`Arc`]
31+
/// this can be implemented more efficiently and avoid many string clones.
32+
fn clone_to_arc(&self) -> Arc<str> {
33+
Arc::from(self.as_str())
34+
}
35+
2736
/// Inserts text `text` into this buffer at character index `char_index`.
2837
///
2938
/// # Notes
@@ -315,3 +324,28 @@ impl TextBuffer for &str {
315324
std::any::TypeId::of::<&str>()
316325
}
317326
}
327+
328+
/// Immutable view of an [`Arc<str>`]
329+
impl TextBuffer for Arc<str> {
330+
fn is_mutable(&self) -> bool {
331+
false
332+
}
333+
334+
fn as_str(&self) -> &str {
335+
self
336+
}
337+
338+
fn clone_to_arc(&self) -> Arc<str> {
339+
self.clone()
340+
}
341+
342+
fn insert_text(&mut self, _text: &str, _ch_idx: usize) -> usize {
343+
0
344+
}
345+
346+
fn delete_char_range(&mut self, _ch_range: Range<usize>) {}
347+
348+
fn type_id(&self) -> std::any::TypeId {
349+
std::any::TypeId::of::<Arc<str>>()
350+
}
351+
}

0 commit comments

Comments
 (0)