Tab identation for multiline text edit (#246)
Lock focus on multiline text edit, and insert tabs on tab char Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
101eed0d67
commit
35c7b09285
6 changed files with 150 additions and 7 deletions
|
@ -141,8 +141,12 @@ pub(crate) struct Focus {
|
||||||
/// The last widget interested in focus.
|
/// The last widget interested in focus.
|
||||||
last_interested: Option<Id>,
|
last_interested: Option<Id>,
|
||||||
|
|
||||||
|
/// If `true`, pressing tab will NOT move focus away from the current widget.
|
||||||
|
is_focus_locked: bool,
|
||||||
|
|
||||||
/// Set at the beginning of the frame, set to `false` when "used".
|
/// Set at the beginning of the frame, set to `false` when "used".
|
||||||
pressed_tab: bool,
|
pressed_tab: bool,
|
||||||
|
|
||||||
/// Set at the beginning of the frame, set to `false` when "used".
|
/// Set at the beginning of the frame, set to `false` when "used".
|
||||||
pressed_shift_tab: bool,
|
pressed_shift_tab: bool,
|
||||||
}
|
}
|
||||||
|
@ -199,6 +203,7 @@ impl Focus {
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
self.id = None;
|
self.id = None;
|
||||||
|
self.is_focus_locked = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,6 +213,7 @@ impl Focus {
|
||||||
modifiers,
|
modifiers,
|
||||||
} = event
|
} = event
|
||||||
{
|
{
|
||||||
|
if !self.is_focus_locked {
|
||||||
if modifiers.shift {
|
if modifiers.shift {
|
||||||
self.pressed_shift_tab = true;
|
self.pressed_shift_tab = true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -216,6 +222,7 @@ impl Focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn end_frame(&mut self, used_ids: &epaint::ahash::AHashMap<Id, Pos2>) {
|
pub(crate) fn end_frame(&mut self, used_ids: &epaint::ahash::AHashMap<Id, Pos2>) {
|
||||||
if let Some(id) = self.id {
|
if let Some(id) = self.id {
|
||||||
|
@ -238,11 +245,11 @@ impl Focus {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self.give_to_next = false;
|
self.give_to_next = false;
|
||||||
} else if self.id == Some(id) {
|
} else if self.id == Some(id) {
|
||||||
if self.pressed_tab {
|
if self.pressed_tab && !self.is_focus_locked {
|
||||||
self.id = None;
|
self.id = None;
|
||||||
self.give_to_next = true;
|
self.give_to_next = true;
|
||||||
self.pressed_tab = false;
|
self.pressed_tab = false;
|
||||||
} else if self.pressed_shift_tab {
|
} else if self.pressed_shift_tab && !self.is_focus_locked {
|
||||||
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
|
self.id_next_frame = self.last_interested; // frame-delay so gained_focus works
|
||||||
self.pressed_shift_tab = false;
|
self.pressed_shift_tab = false;
|
||||||
}
|
}
|
||||||
|
@ -302,11 +309,26 @@ impl Memory {
|
||||||
self.interaction.focus.id == Some(id)
|
self.interaction.focus.id == Some(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lock_focus(&mut self, id: Id, b: bool) {
|
||||||
|
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||||
|
self.interaction.focus.is_focus_locked = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_lock_focus(&mut self, id: Id) -> bool {
|
||||||
|
if self.had_focus_last_frame(id) && self.has_focus(id) {
|
||||||
|
self.interaction.focus.is_focus_locked
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Give keyboard focus to a specific widget.
|
/// Give keyboard focus to a specific widget.
|
||||||
/// See also [`crate::Response::request_focus`].
|
/// See also [`crate::Response::request_focus`].
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn request_focus(&mut self, id: Id) {
|
pub fn request_focus(&mut self, id: Id) {
|
||||||
self.interaction.focus.id = Some(id);
|
self.interaction.focus.id = Some(id);
|
||||||
|
self.interaction.focus.is_focus_locked = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surrender keyboard focus for a specific widget.
|
/// Surrender keyboard focus for a specific widget.
|
||||||
|
@ -315,6 +337,7 @@ impl Memory {
|
||||||
pub fn surrender_focus(&mut self, id: Id) {
|
pub fn surrender_focus(&mut self, id: Id) {
|
||||||
if self.interaction.focus.id == Some(id) {
|
if self.interaction.focus.id == Some(id) {
|
||||||
self.interaction.focus.id = None;
|
self.interaction.focus.id = None;
|
||||||
|
self.interaction.focus.is_focus_locked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -934,6 +934,22 @@ impl Ui {
|
||||||
TextEdit::multiline(text).ui(self)
|
TextEdit::multiline(text).ui(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A `TextEdit` for code editing.
|
||||||
|
///
|
||||||
|
/// This will be multiline, monospace, and will insert tabs instead of moving focus.
|
||||||
|
///
|
||||||
|
/// See also [`TextEdit::code_editor`].
|
||||||
|
pub fn code_editor(&mut self, text: &mut String) -> Response {
|
||||||
|
self.add(TextEdit::multiline(text).code_editor())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `TextEdit` for code editing with configurable `Tab` management.
|
||||||
|
///
|
||||||
|
/// Se also [`TextEdit::code_editor_with_config`].
|
||||||
|
pub fn code_editor_with_config(&mut self, text: &mut String, config: CodingConfig) -> Response {
|
||||||
|
self.add(TextEdit::multiline(text).code_editor_with_config(config))
|
||||||
|
}
|
||||||
|
|
||||||
/// Usage: `if ui.button("Click me").clicked() { … }`
|
/// Usage: `if ui.button("Click me").clicked() { … }`
|
||||||
///
|
///
|
||||||
/// Shortcut for `add(Button::new(text))`
|
/// Shortcut for `add(Button::new(text))`
|
||||||
|
|
|
@ -108,6 +108,12 @@ impl CCursorPair {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
pub struct CodingConfig {
|
||||||
|
pub tab_moves_focus: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// 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`].
|
||||||
|
@ -140,6 +146,7 @@ pub struct TextEdit<'t> {
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
desired_width: Option<f32>,
|
desired_width: Option<f32>,
|
||||||
desired_height_rows: usize,
|
desired_height_rows: usize,
|
||||||
|
tab_moves_focus: bool,
|
||||||
}
|
}
|
||||||
impl<'t> TextEdit<'t> {
|
impl<'t> TextEdit<'t> {
|
||||||
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
|
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
|
||||||
|
@ -171,6 +178,7 @@ impl<'t> TextEdit<'t> {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
desired_width: None,
|
desired_width: None,
|
||||||
desired_height_rows: 1,
|
desired_height_rows: 1,
|
||||||
|
tab_moves_focus: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,9 +197,37 @@ impl<'t> TextEdit<'t> {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
desired_width: None,
|
desired_width: None,
|
||||||
desired_height_rows: 4,
|
desired_height_rows: 4,
|
||||||
|
tab_moves_focus: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When this is true, then pass focus to the next
|
||||||
|
/// widget.
|
||||||
|
pub fn tab_moves_focus(mut self, b: bool) -> Self {
|
||||||
|
self.tab_moves_focus = b;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `TextEdit` focused on code editing.
|
||||||
|
/// By default it comes with:
|
||||||
|
/// - monospaced font
|
||||||
|
/// - focus lock
|
||||||
|
pub fn code_editor(self) -> Self {
|
||||||
|
self.text_style(TextStyle::Monospace).tab_moves_focus(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a `TextEdit` focused on code editing with configurable `Tab` management.
|
||||||
|
///
|
||||||
|
/// Shortcut for:
|
||||||
|
/// ```rust, ignore
|
||||||
|
/// egui::TextEdit::multiline(code_snippet)
|
||||||
|
/// .code_editor()
|
||||||
|
/// .tab_moves_focus(tab_moves_focus);
|
||||||
|
/// ```
|
||||||
|
pub fn code_editor_with_config(self, config: CodingConfig) -> Self {
|
||||||
|
self.code_editor().tab_moves_focus(config.tab_moves_focus)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn id(mut self, id: Id) -> Self {
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
self.id = Some(id);
|
self.id = Some(id);
|
||||||
self
|
self
|
||||||
|
@ -312,6 +348,7 @@ impl<'t> TextEdit<'t> {
|
||||||
enabled,
|
enabled,
|
||||||
desired_width,
|
desired_width,
|
||||||
desired_height_rows,
|
desired_height_rows,
|
||||||
|
tab_moves_focus,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
|
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
|
||||||
|
@ -417,6 +454,8 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
let mut text_cursor = None;
|
let mut text_cursor = None;
|
||||||
if ui.memory().has_focus(id) && enabled {
|
if ui.memory().has_focus(id) && enabled {
|
||||||
|
ui.memory().lock_focus(id, !tab_moves_focus);
|
||||||
|
|
||||||
let mut cursorp = state
|
let mut cursorp = state
|
||||||
.cursorp
|
.cursorp
|
||||||
.map(|cursorp| {
|
.map(|cursorp| {
|
||||||
|
@ -466,12 +505,27 @@ impl<'t> TextEdit<'t> {
|
||||||
&& text_to_insert != "\r"
|
&& text_to_insert != "\r"
|
||||||
{
|
{
|
||||||
let mut ccursor = delete_selected(text, &cursorp);
|
let mut ccursor = delete_selected(text, &cursorp);
|
||||||
|
|
||||||
insert_text(&mut ccursor, text, text_to_insert);
|
insert_text(&mut ccursor, text, text_to_insert);
|
||||||
Some(CCursorPair::one(ccursor))
|
Some(CCursorPair::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::Key {
|
||||||
|
key: Key::Tab,
|
||||||
|
pressed: true,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if multiline && ui.memory().has_lock_focus(id) {
|
||||||
|
let mut ccursor = delete_selected(text, &cursorp);
|
||||||
|
|
||||||
|
insert_text(&mut ccursor, text, "\t");
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
Event::Key {
|
Event::Key {
|
||||||
key: Key::Enter,
|
key: Key::Enter,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
|
|
|
@ -22,8 +22,11 @@ pub struct Widgets {
|
||||||
radio: Enum,
|
radio: Enum,
|
||||||
angle: f32,
|
angle: f32,
|
||||||
color: Color32,
|
color: Color32,
|
||||||
|
show_password: bool,
|
||||||
single_line_text_input: String,
|
single_line_text_input: String,
|
||||||
multiline_text_input: String,
|
multiline_text_input: String,
|
||||||
|
tab_moves_focus: bool,
|
||||||
|
code_snippet: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Widgets {
|
impl Default for Widgets {
|
||||||
|
@ -35,7 +38,31 @@ impl Default for Widgets {
|
||||||
angle: std::f32::consts::TAU / 3.0,
|
angle: std::f32::consts::TAU / 3.0,
|
||||||
color: (Rgba::from_rgb(0.0, 1.0, 0.5) * 0.75).into(),
|
color: (Rgba::from_rgb(0.0, 1.0, 0.5) * 0.75).into(),
|
||||||
single_line_text_input: "Hello World!".to_owned(),
|
single_line_text_input: "Hello World!".to_owned(),
|
||||||
|
show_password: false,
|
||||||
|
tab_moves_focus: false,
|
||||||
|
|
||||||
multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
|
multiline_text_input: "Text can both be so wide that it needs a line break, but you can also add manual line break by pressing enter, creating new paragraphs.\nThis is the start of the next paragraph.\n\nClick me to edit me!".to_owned(),
|
||||||
|
code_snippet: r#"// Full identation blocks
|
||||||
|
// Spaces Spaces Spaces
|
||||||
|
// Tab Tab Tab
|
||||||
|
// Spaces Tab Spaces
|
||||||
|
// Tab Spaces Tab
|
||||||
|
|
||||||
|
// Partial identation blocks
|
||||||
|
// Space Tab
|
||||||
|
// Space Space Tab
|
||||||
|
// Space Space Space Tab
|
||||||
|
// Space / / Space
|
||||||
|
// Space Space / /
|
||||||
|
// Space Space Space /
|
||||||
|
|
||||||
|
// Use the configs above to play with the tab management
|
||||||
|
// Also existing tabs are kept as tabs.
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("Hello world!");
|
||||||
|
}
|
||||||
|
"#.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +171,26 @@ impl Widgets {
|
||||||
ui.memory().id_data.insert(show_password_id, show_password);
|
ui.memory().id_data.insert(show_password_id, show_password);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
ui.label("Multiline text input:");
|
ui.label("Multiline text input:");
|
||||||
ui.text_edit_multiline(&mut self.multiline_text_input);
|
ui.text_edit_multiline(&mut self.multiline_text_input);
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label("Code editor:");
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
ui.checkbox(&mut self.tab_moves_focus, "Tabs moves focus");
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.code_editor_with_config(
|
||||||
|
&mut self.code_snippet,
|
||||||
|
CodingConfig {
|
||||||
|
tab_moves_focus: self.tab_moves_focus,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ impl FontImpl {
|
||||||
|
|
||||||
if c == '\t' {
|
if c == '\t' {
|
||||||
if let Some(space) = self.glyph_info(' ') {
|
if let Some(space) = self.glyph_info(' ') {
|
||||||
glyph_info.advance_width = 4.0 * space.advance_width;
|
glyph_info.advance_width = crate::text::MAX_TAB_SIZE * space.advance_width;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +285,7 @@ impl Font {
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
if !self.fonts.is_empty() {
|
if !self.fonts.is_empty() {
|
||||||
let (font_index, glyph_info) = self.glyph_info(c);
|
let (font_index, glyph_info) = self.glyph_info(c);
|
||||||
|
|
||||||
let font_impl = &self.fonts[font_index];
|
let font_impl = &self.fonts[font_index];
|
||||||
|
|
||||||
if let Some(last_glyph_id) = last_glyph_id {
|
if let Some(last_glyph_id) = last_glyph_id {
|
||||||
|
|
|
@ -5,6 +5,9 @@ mod font;
|
||||||
mod fonts;
|
mod fonts;
|
||||||
mod galley;
|
mod galley;
|
||||||
|
|
||||||
|
/// Default size for a `\t` character.
|
||||||
|
pub const MAX_TAB_SIZE: f32 = 4.0;
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},
|
||||||
galley::{Galley, Row},
|
galley::{Galley, Row},
|
||||||
|
|
Loading…
Reference in a new issue