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};
|
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.
|
/// A text region that the user can edit the contents of.
|
||||||
///
|
///
|
||||||
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
|
/// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`].
|
||||||
|
|
@ -71,7 +73,7 @@ pub struct TextEdit<'t> {
|
||||||
id_salt: Option<Id>,
|
id_salt: Option<Id>,
|
||||||
font_selection: FontSelection,
|
font_selection: FontSelection,
|
||||||
text_color: Option<Color32>,
|
text_color: Option<Color32>,
|
||||||
layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>,
|
layouter: Option<LayouterFn<'t>>,
|
||||||
password: bool,
|
password: bool,
|
||||||
frame: bool,
|
frame: bool,
|
||||||
margin: Margin,
|
margin: Margin,
|
||||||
|
|
@ -261,8 +263,8 @@ impl<'t> TextEdit<'t> {
|
||||||
/// # egui::__run_test_ui(|ui| {
|
/// # egui::__run_test_ui(|ui| {
|
||||||
/// # let mut my_code = String::new();
|
/// # let mut my_code = String::new();
|
||||||
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
|
/// # fn my_memoized_highlighter(s: &str) -> egui::text::LayoutJob { Default::default() }
|
||||||
/// 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::text::LayoutJob = my_memoized_highlighter(string);
|
/// let mut layout_job: egui::text::LayoutJob = my_memoized_highlighter(buf.as_str());
|
||||||
/// layout_job.wrap.max_width = wrap_width;
|
/// layout_job.wrap.max_width = wrap_width;
|
||||||
/// ui.fonts(|f| f.layout_job(layout_job))
|
/// ui.fonts(|f| f.layout_job(layout_job))
|
||||||
/// };
|
/// };
|
||||||
|
|
@ -270,7 +272,10 @@ impl<'t> TextEdit<'t> {
|
||||||
/// # });
|
/// # });
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[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.layouter = Some(layouter);
|
||||||
|
|
||||||
self
|
self
|
||||||
|
|
@ -510,8 +515,8 @@ impl TextEdit<'_> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let font_id_clone = font_id.clone();
|
let font_id_clone = font_id.clone();
|
||||||
let mut default_layouter = move |ui: &Ui, text: &str, wrap_width: f32| {
|
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
|
||||||
let text = mask_if_password(password, text);
|
let text = mask_if_password(password, text.as_str());
|
||||||
let layout_job = if multiline {
|
let layout_job = if multiline {
|
||||||
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -522,7 +527,7 @@ impl TextEdit<'_> {
|
||||||
|
|
||||||
let layouter = layouter.unwrap_or(&mut default_layouter);
|
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 {
|
let desired_inner_width = if clip_text {
|
||||||
wrap_width // visual clipping with scroll in singleline input.
|
wrap_width // visual clipping with scroll in singleline input.
|
||||||
|
|
@ -879,7 +884,7 @@ fn events(
|
||||||
state: &mut TextEditState,
|
state: &mut TextEditState,
|
||||||
text: &mut dyn TextBuffer,
|
text: &mut dyn TextBuffer,
|
||||||
galley: &mut Arc<Galley>,
|
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,
|
id: Id,
|
||||||
wrap_width: f32,
|
wrap_width: f32,
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
|
|
@ -1094,7 +1099,7 @@ fn events(
|
||||||
any_change = true;
|
any_change = true;
|
||||||
|
|
||||||
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
// 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:
|
// Set cursor_range using new galley:
|
||||||
cursor_range = new_ccursor_range;
|
cursor_range = new_ccursor_range;
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,39 @@ pub trait TextBuffer {
|
||||||
self.delete_selected(&CCursorRange::two(min, max))
|
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 {
|
impl TextBuffer for String {
|
||||||
|
|
@ -218,6 +251,10 @@ impl TextBuffer for String {
|
||||||
fn take(&mut self) -> String {
|
fn take(&mut self) -> String {
|
||||||
std::mem::take(self)
|
std::mem::take(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn type_id(&self) -> std::any::TypeId {
|
||||||
|
std::any::TypeId::of::<Self>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextBuffer for Cow<'_, str> {
|
impl TextBuffer for Cow<'_, str> {
|
||||||
|
|
@ -248,6 +285,10 @@ impl TextBuffer for Cow<'_, str> {
|
||||||
fn take(&mut self) -> String {
|
fn take(&mut self) -> String {
|
||||||
std::mem::take(self).into_owned()
|
std::mem::take(self).into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn type_id(&self) -> std::any::TypeId {
|
||||||
|
std::any::TypeId::of::<Cow<'_, str>>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Immutable view of a `&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 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(
|
let mut layout_job = egui_extras::syntax_highlighting::highlight(
|
||||||
ui.ctx(),
|
ui.ctx(),
|
||||||
ui.style(),
|
ui.style(),
|
||||||
&theme,
|
&theme,
|
||||||
string,
|
buf.as_str(),
|
||||||
language,
|
language,
|
||||||
);
|
);
|
||||||
layout_job.wrap.max_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,8 @@ impl EasyMarkEditor {
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let response = if self.highlight_editor {
|
let response = if self.highlight_editor {
|
||||||
let mut layouter = |ui: &egui::Ui, easymark: &str, wrap_width: f32| {
|
let mut layouter = |ui: &egui::Ui, easymark: &dyn TextBuffer, wrap_width: f32| {
|
||||||
let mut layout_job = highlighter.highlight(ui.style(), easymark);
|
let mut layout_job = highlighter.highlight(ui.style(), easymark.as_str());
|
||||||
layout_job.wrap.max_width = wrap_width;
|
layout_job.wrap.max_width = wrap_width;
|
||||||
ui.fonts(|f| f.layout_job(layout_job))
|
ui.fonts(|f| f.layout_job(layout_job))
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue