说在前面
- rust新手,egui没啥找到啥教程,这里自己记录下学习过程
- 环境:windows11 22H2
- rust版本:rustc 1.71.1
- egui版本:0.22.0
- eframe版本:0.22.0
- 上一篇:这里
TextEdit
-
文本编辑框
-
其定义为:
pub struct TextEdit<'t> { text: &'t mut dyn TextBuffer, hint_text: WidgetText, id: Option<Id>, id_source: Option<Id>, font_selection: FontSelection, text_color: Option<Color32>, // 文本颜色 layouter: Option<&'t mut dyn FnMut(&Ui, &str, f32) -> Arc<Galley>>, password: bool, // 是否是密码 frame: bool, margin: Vec2, multiline: bool, // 是否支持多行文本 interactive: bool, // 是否可编辑 desired_width: Option<f32>, // 宽度 desired_height_rows: usize, // 文本行数 lock_focus: bool, cursor_at_end: bool, min_size: Vec2, // 最小大小 align: Align2, // 边距 clip_text: bool, // 显示时是否进行裁剪 char_limit: usize, // 文字上限 }
用起来可能是个简单的东西,但是实际上很是复杂,首先我们来看看它的外观以及用法
-
在
app.rs
中,我们是通过以下方式添加的:ui.text_edit_singleline(label);
它添加的是一个简单的单行输入框:
pub fn singleline(text: &'t mut dyn TextBuffer) -> Self { Self { desired_height_rows: 1, // 文本行数 multiline: false, // 是否多行,否 clip_text: true, // 显示时是否裁剪文本,是 ..Self::multiline(text) } }
-
同样,我们可以通过
ui.add()
的方式来自定义属性 -
clip_text
ui.add(egui::TextEdit::singleline(label).clip_text(false));
效果如下,输入文本后,文本框宽度将随着输入文本扩展
-
interactive
ui.add(egui::TextEdit::singleline(label) .clip_text(false) .interactive(false));
效果如下,文本框将不可编辑,但是同样也不能选中(也就不能复制)
-
我们可以添加一个多行文本输入框看看:
ui.add(egui::TextEdit::multiline(label));
效果如下
由于我们使用的是同一个可变引用,所以在任意一个输入框输入文本时,两边会同时改变 -
另外,我们也可以实现不可编辑但是可以选中的效果:
ui.add(egui::TextEdit::multiline(&mut label.as_str()));
- 这里有个地方无法理解,
&mut label.as_str()
的类型是&mut &str
,这是个啥?对&str
的可变引用?&mut &str
和&mut str
的区别是啥? - 使用
&str
倒是能理解,因为text: &'t mut dyn TextBuffer
是限定了TextBuffer
特征的,而egui
只为String
和&str
实现了该特征,并且一个可变,一个不可变,符合预期。impl TextBuffer for String { fn is_mutable(&self) -> bool { true } // .. } impl<'a> TextBuffer for &'a str { fn is_mutable(&self) -> bool { false } // .. }
- 这里有个地方无法理解,
-
输入框也支持对事件进行响应
let response = ui.add(egui::TextEdit::singleline(&mut my_string)); if response.changed() { // … } if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { // … }
-
进阶用法
let output = egui::TextEdit::singleline(label).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 = label.char_range(selected_chars); ui.label("Selected text: "); ui.monospace(selected_text); }
变量绑定过程
- 初次接触
update
函数以及输入框,对于label
变量是怎样和文本输入框的内容绑定在一起还是很感兴趣的,看了一些源码后有一些猜想,这里记录下 - 首先,text是特征对象
TextBuffer
的可变引用,而特征TextBuffer
则有一些关键的方法,例如insert_text()
:
通过这些方法可以对变量值进行修改,而后就是调用这些方法的过程是怎样的?impl TextBuffer for String { fn is_mutable(&self) -> bool { true } fn as_str(&self) -> &str { self.as_ref() } fn insert_text(&mut self, text: &str, char_index: usize) -> usize { // Get the byte index from the character index let byte_idx = self.byte_index_from_char_index(char_index); // Then insert the string self.insert_str(byte_idx, text); text.chars().count() } fn delete_char_range(&mut self, char_range: Range<usize>) { assert!(char_range.start <= char_range.end); // Get both byte indices let byte_start = self.byte_index_from_char_index(char_range.start); let byte_end = self.byte_index_from_char_index(char_range.end); // Then drain all characters within this range self.drain(byte_start..byte_end); } fn clear(&mut self) { self.clear(); } fn replace(&mut self, text: &str) { *self = text.to_owned(); } fn take(&mut self) -> String { std::mem::take(self) } }
- 查找
insert_text()
方法的引用,可以找到一个events()
函数:/// Check for (keyboard) events to edit the cursor and/or text. /// 监听文本框中光标/文本对应的(键盘)事件 fn events()
- 也就是说,在我们使用键盘输入字符时,会触发对应的事件,从而调用到对应的
insert_text()
方法,从而改变对应的变量值。 - 但是,这其中又有另外一个问题:在前面的文章中有提到,
update
函数也是触发了对应的事件后才会被调用的 ,而我们的变量label
是在update
函数开始才进行的绑定,那么,这个输入文本 到 对应变量值改变的具体过程(顺序)是怎样的呢? - 首先说猜想 (后面证明该猜想是错误的)
- 应用启动,
update
会首次调用一次,这个时候,我们的变量label
通过层层转移,最终显示到文本框中。 - 这个时候输入字符,
eframe
监听到事件,将事件通知egui
进行分发 events()
函数触发,修改对应的值egui
调用update
函数,更新ui
- 应用启动,
- 上面的猜想中,主要的点在于,变量值在
update
函数前就被更新了,所以我们可以添加日志进行验证:
日志输出为:fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { let Self { label, value } = self; log::error!("{}", label); // ... log::error!("update end {}", label); }
可以看到,在输入字符的那一次update中,变量值在函数开始时并没有发生变化,也就是说刚刚的猜想是错的🤡[2023-08-27T09:42:45Z ERROR demo_app::app] o0olele [2023-08-27T09:42:45Z ERROR demo_app::app] update end o0olele [2023-08-27T09:42:45Z ERROR demo_app::app] o0olele [2023-08-27T09:42:45Z ERROR demo_app::app] update end o0olele1
- 那是怎么回事呢?
- 让我们回过头来看看
events()
的引用,居然是在show()
函数中被调用的,而show()
是在update中调用的 - 所以实际的过程应该是:
- 应用启动,
update
会首次调用一次,这个时候,我们的变量label
通过层层转移,最终显示到文本框中。 - 这个时候输入字符,
eframe
监听到事件,将事件通知egui
进行分发并调用update
函数 label
变量再次进行绑定- 之后,在
TextEdit
对应的show()
方法中,检测到对应的事件,进而修改对应的变量值 - 更新ui
- 应用启动,
参考
- Rust: the weird but safe string
- TextEdit