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](https://github.com/damus-io/notedeck/pull/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 <kernelkind@gmail.com>
This commit is contained in:
parent
d78fc39386
commit
fe631ff9ea
|
|
@ -19,6 +19,8 @@ use crate::{
|
|||
|
||||
use super::{TextEditOutput, TextEditState};
|
||||
|
||||
type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>;
|
||||
|
||||
/// A text region that the user can edit the contents of.
|
||||
///
|
||||
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
|
||||
|
|
@ -71,7 +73,7 @@ pub struct TextEdit<'t> {
|
|||
id_salt: Option<Id>,
|
||||
font_selection: FontSelection,
|
||||
text_color: Option<Color32>,
|
||||
layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
|
||||
layouter: Option<LayouterFn<'t>>,
|
||||
password: bool,
|
||||
frame: bool,
|
||||
margin: Margin,
|
||||
|
|
@ -261,8 +263,8 @@ impl<'t> TextEdit<'t> {
|
|||
/// # egui::__run_test_ui(|ui| {
|
||||
/// # let mut my_code = String::new();
|
||||
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
|
||||
/// let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(string);
|
||||
/// let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str());
|
||||
/// layout_job.wrap.max_width = wrap_width;
|
||||
/// ui.fonts(|f| f.layout_job(layout_job))
|
||||
/// };
|
||||
|
|
@ -270,7 +272,10 @@ impl<'t> TextEdit<'t> {
|
|||
/// # });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn layouter(mut self, layouter: &'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>) -> Self {
|
||||
pub fn layouter(
|
||||
mut self,
|
||||
layouter: &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
|
||||
) -> Self {
|
||||
self.layouter = Some(layouter);
|
||||
|
||||
self
|
||||
|
|
@ -510,8 +515,8 @@ impl TextEdit<'_> {
|
|||
};
|
||||
|
||||
let font_id_clone = font_id.clone();
|
||||
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
|
||||
let text = mask_if_password(password, text);
|
||||
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
|
||||
let text = mask_if_password(password, text.as_str());
|
||||
let layout_job = if multiline {
|
||||
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
||||
} else {
|
||||
|
|
@ -522,7 +527,7 @@ impl TextEdit<'_> {
|
|||
|
||||
let layouter = layouter.unwrap_or(&mut default_layouter);
|
||||
|
||||
let mut galley = layouter(ui, text.as_str(), wrap_width);
|
||||
let mut galley = layouter(ui, text, wrap_width);
|
||||
|
||||
let desired_inner_width = if clip_text {
|
||||
wrap_width // visual clipping with scroll in singleline input.
|
||||
|
|
@ -879,7 +884,7 @@ fn events(
|
|||
state: &mut TextEditState,
|
||||
text: &mut dyn TextBuffer,
|
||||
galley: &mut Arc<Galley>,
|
||||
layouter: &mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>,
|
||||
layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley>,
|
||||
id: Id,
|
||||
wrap_width: f32,
|
||||
multiline: bool,
|
||||
|
|
@ -1094,7 +1099,7 @@ fn events(
|
|||
any_change = true;
|
||||
|
||||
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
||||
*galley = layouter(ui, text.as_str(), wrap_width);
|
||||
*galley = layouter(ui, text, wrap_width);
|
||||
|
||||
// Set cursor_range using new galley:
|
||||
cursor_range = new_ccursor_range;
|
||||
|
|
|
|||
|
|
@ -172,6 +172,39 @@ pub trait TextBuffer {
|
|||
self.delete_selected(&CCursorRange::two(min, max))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a unique identifier for the implementing type.
|
||||
///
|
||||
/// This is useful for downcasting from this trait to the implementing type.
|
||||
/// Here is an example usage:
|
||||
/// ```
|
||||
/// use egui::TextBuffer;
|
||||
/// use std::any::TypeId;
|
||||
///
|
||||
/// struct ExampleBuffer {}
|
||||
///
|
||||
/// impl TextBuffer for ExampleBuffer {
|
||||
/// fn is_mutable(&self) -> bool { unimplemented!() }
|
||||
/// fn as_str(&self) -> &str { unimplemented!() }
|
||||
/// fn insert_text(&mut self, text: &str, char_index: usize) -> usize { unimplemented!() }
|
||||
/// fn delete_char_range(&mut self, char_range: std::ops::Range<usize>) { unimplemented!() }
|
||||
///
|
||||
/// // Implement it like the following:
|
||||
/// fn type_id(&self) -> TypeId {
|
||||
/// TypeId::of::<Self>()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Example downcast:
|
||||
/// pub fn downcast_example(buffer: &dyn TextBuffer) -> Option<&ExampleBuffer> {
|
||||
/// if buffer.type_id() == TypeId::of::<ExampleBuffer>() {
|
||||
/// unsafe { Some(&*(buffer as *const dyn TextBuffer as *const ExampleBuffer)) }
|
||||
/// } else {
|
||||
/// None
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
fn type_id(&self) -> std::any::TypeId;
|
||||
}
|
||||
|
||||
impl TextBuffer for String {
|
||||
|
|
@ -218,6 +251,10 @@ impl TextBuffer for String {
|
|||
fn take(&mut self) -> String {
|
||||
std::mem::take(self)
|
||||
}
|
||||
|
||||
fn type_id(&self) -> std::any::TypeId {
|
||||
std::any::TypeId::of::<Self>()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextBuffer for Cow<'_, str> {
|
||||
|
|
@ -248,6 +285,10 @@ impl TextBuffer for Cow<'_, str> {
|
|||
fn take(&mut self) -> String {
|
||||
std::mem::take(self).into_owned()
|
||||
}
|
||||
|
||||
fn type_id(&self) -> std::any::TypeId {
|
||||
std::any::TypeId::of::<Cow<'_, str>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable view of a `&str`!
|
||||
|
|
@ -265,4 +306,8 @@ impl TextBuffer for &str {
|
|||
}
|
||||
|
||||
fn delete_char_range(&mut self, _ch_range: Range<usize>) {}
|
||||
|
||||
fn type_id(&self) -> std::any::TypeId {
|
||||
std::any::TypeId::of::<&str>()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,12 +76,12 @@ impl crate::View for CodeEditor {
|
|||
});
|
||||
});
|
||||
|
||||
let mut layouter = |ui: &egui::Ui, string: &str, wrap_width: f32| {
|
||||
let mut layouter = |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| {
|
||||
let mut layout_job = egui_extras::syntax_highlighting::highlight(
|
||||
ui.ctx(),
|
||||
ui.style(),
|
||||
&theme,
|
||||
string,
|
||||
buf.as_str(),
|
||||
language,
|
||||
);
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ impl EasyMarkEditor {
|
|||
} = self;
|
||||
|
||||
let response = if self.highlight_editor {
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
||||
let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| {
|
||||
let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str());
|
||||
layout_job.wrap.max_width = wrap_width;
|
||||
ui.fonts(|f| f.layout_job(layout_job))
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue