Skip to content

Commit fe631ff

Browse files
authored
Use TextBuffer for layouter in TextEdit instead of &str (#5712)
This change allows `layouter` to use the `TextBuffer` instead of `&str` in the closure. It is necessary when layout decisions depend on more than just the raw string content, such as metadata stored in the concrete type implementing `TextBuffer`. In [our use case](damus-io/notedeck#723), we needed this to support mention highlighting when a user selects a mention. Since mentions can contain spaces, determining mention boundaries from the `&str` alone is impossible. Instead, we use the `TextBuffer` implementation to retrieve the correct bounds. See the video below for a demonstration: https://github.com/user-attachments/assets/3cba2906-5546-4b52-b728-1da9c56a83e1 # Breaking change This PR introduces a breaking change to the `layouter` function in `TextEdit`. Previous API: ```rust pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>) -> Self ``` New API: ```rust pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>) -> Self ``` ## Impact on Existing Code • Any existing usage of `layouter` will **no longer compile**. • Callers must update their closures to use `&dyn TextBuffer` instead of `&str`. ## Migration Guide Before: ```rust let mut layouter = |ui: &Ui, text: &str, wrap_width: f32| {     let layout_job = my_highlighter(text);     layout_job.wrap.max_width = wrap_width;     ui.fonts(|f| f.layout_job(layout_job)) }; ``` After: ```rust let mut layouter = |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { let layout_job = my_highlighter(text.as_str()); layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; ``` --- * There is not an issue for this change. * [x] I have followed the instructions in the PR template Signed-off-by: kernelkind <[email protected]>
1 parent d78fc39 commit fe631ff

File tree

4 files changed

+63
-13
lines changed

4 files changed

+63
-13
lines changed

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

+14-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use crate::{
1919

2020
use super::{TextEditOutput, TextEditState};
2121

22+
type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>;
23+
2224
/// A text region that the user can edit the contents of.
2325
///
2426
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
@@ -71,7 +73,7 @@ pub struct TextEdit<'t> {
7173
id_salt: Option<Id>,
7274
font_selection: FontSelection,
7375
text_color: Option<Color32>,
74-
layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
76+
layouter: Option<LayouterFn<'t>>,
7577
password: bool,
7678
frame: bool,
7779
margin: Margin,
@@ -261,16 +263,19 @@ impl<'t> TextEdit<'t> {
261263
/// # egui::__run_test_ui(|ui| {
262264
/// # let mut my_code = String::new();
263265
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
264-
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
265-
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
266+
/// let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
267+
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str());
266268
/// layout_job.wrap.max_width = wrap_width;
267269
/// ui.fonts(|f| f.layout_job(layout_job))
268270
/// };
269271
/// ui.add(egui::TextEdit::multiline(&mut my_code).layouter(&mut layouter));
270272
/// # });
271273
/// ```
272274
#[inline]
273-
pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>) -> Self {
275+
pub fn layouter(
276+
mut self,
277+
layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
278+
) -> Self {
274279
self.layouter = Some(layouter);
275280

276281
self
@@ -510,8 +515,8 @@ impl TextEdit<'_> {
510515
};
511516

512517
let font_id_clone = font_id.clone();
513-
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
514-
let text = mask_if_password(password, text);
518+
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
519+
let text = mask_if_password(password, text.as_str());
515520
let layout_job = if multiline {
516521
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
517522
} else {
@@ -522,7 +527,7 @@ impl TextEdit<'_> {
522527

523528
let layouter = layouter.unwrap_or(&mut default_layouter);
524529

525-
let mut galley = layouter(ui, text.as_str(), wrap_width);
530+
let mut galley = layouter(ui, text, wrap_width);
526531

527532
let desired_inner_width = if clip_text {
528533
wrap_width // visual clipping with scroll in singleline input.
@@ -879,7 +884,7 @@ fn events(
879884
state: &mut TextEditState,
880885
text: &mut dyn TextBuffer,
881886
galley: &mut Arc<Galley>,
882-
layouter: &mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>,
887+
layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
883888
id: Id,
884889
wrap_width: f32,
885890
multiline: bool,
@@ -1094,7 +1099,7 @@ fn events(
10941099
any_change = true;
10951100

10961101
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
1097-
*galley = layouter(ui, text.as_str(), wrap_width);
1102+
*galley = layouter(ui, text, wrap_width);
10981103

10991104
// Set cursor_range using new galley:
11001105
cursor_range = new_ccursor_range;

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

+45
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,39 @@ pub trait TextBuffer {
172172
self.delete_selected(&CCursorRange::two(min, max))
173173
}
174174
}
175+
176+
/// Returns a unique identifier for the implementing type.
177+
///
178+
/// This is useful for downcasting from this trait to the implementing type.
179+
/// Here is an example usage:
180+
/// ```
181+
/// use egui::TextBuffer;
182+
/// use std::any::TypeId;
183+
///
184+
/// struct ExampleBuffer {}
185+
///
186+
/// impl TextBuffer for ExampleBuffer {
187+
/// fn is_mutable(&self) -> bool { unimplemented!() }
188+
/// fn as_str(&self) -> &str { unimplemented!() }
189+
/// fn insert_text(&mut self, text: &str, char_index: usize) -> usize { unimplemented!() }
190+
/// fn delete_char_range(&mut self, char_range: std::ops::Range<usize>) { unimplemented!() }
191+
///
192+
/// // Implement it like the following:
193+
/// fn type_id(&self) -> TypeId {
194+
/// TypeId::of::<Self>()
195+
/// }
196+
/// }
197+
///
198+
/// // Example downcast:
199+
/// pub fn downcast_example(buffer: &dyn TextBuffer) -> Option<&ExampleBuffer> {
200+
/// if buffer.type_id() == TypeId::of::<ExampleBuffer>() {
201+
/// unsafe { Some(&*(buffer as *const dyn TextBuffer as *const ExampleBuffer)) }
202+
/// } else {
203+
/// None
204+
/// }
205+
/// }
206+
/// ```
207+
fn type_id(&self) -> std::any::TypeId;
175208
}
176209

177210
impl TextBuffer for String {
@@ -218,6 +251,10 @@ impl TextBuffer for String {
218251
fn take(&mut self) -> String {
219252
std::mem::take(self)
220253
}
254+
255+
fn type_id(&self) -> std::any::TypeId {
256+
std::any::TypeId::of::<Self>()
257+
}
221258
}
222259

223260
impl TextBuffer for Cow<'_, str> {
@@ -248,6 +285,10 @@ impl TextBuffer for Cow<'_, str> {
248285
fn take(&mut self) -> String {
249286
std::mem::take(self).into_owned()
250287
}
288+
289+
fn type_id(&self) -> std::any::TypeId {
290+
std::any::TypeId::of::<Cow<'_, str>>()
291+
}
251292
}
252293

253294
/// Immutable view of a `&str`!
@@ -265,4 +306,8 @@ impl TextBuffer for &str {
265306
}
266307

267308
fn delete_char_range(&mut self, _ch_range: Range<usize>) {}
309+
310+
fn type_id(&self) -> std::any::TypeId {
311+
std::any::TypeId::of::<&str>()
312+
}
268313
}

crates/egui_demo_lib/src/demo/code_editor.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ impl crate::View for CodeEditor {
7676
});
7777
});
7878

79-
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
79+
let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
8080
let mut layout_job = egui_extras::syntax_highlighting::highlight(
8181
ui.ctx(),
8282
ui.style(),
8383
&theme,
84-
string,
84+
buf.as_str(),
8585
language,
8686
);
8787
layout_job.wrap.max_width = wrap_width;

crates/egui_demo_lib/src/easy_mark/easy_mark_editor.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ impl EasyMarkEditor {
8080
} = self;
8181

8282
let response = if self.highlight_editor {
83-
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
84-
let mut layout_job = highlighter.highlight(ui.style(), easymark);
83+
let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| {
84+
let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str());
8585
layout_job.wrap.max_width = wrap_width;
8686
ui.fonts(|f| f.layout_job(layout_job))
8787
};

0 commit comments

Comments
 (0)