diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 608b0b0fe30..54f759e040b 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -141,8 +141,12 @@ pub(crate) struct Focus { /// The last widget interested in focus. last_interested: Option, + /// 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". pressed_tab: bool, + /// Set at the beginning of the frame, set to `false` when "used". pressed_shift_tab: bool, } @@ -199,6 +203,7 @@ impl Focus { } ) { self.id = None; + self.is_focus_locked = false; break; } @@ -208,10 +213,12 @@ impl Focus { modifiers, } = event { - if modifiers.shift { - self.pressed_shift_tab = true; - } else { - self.pressed_tab = true; + if !self.is_focus_locked { + if modifiers.shift { + self.pressed_shift_tab = true; + } else { + self.pressed_tab = true; + } } } } @@ -238,11 +245,11 @@ impl Focus { self.id = Some(id); self.give_to_next = false; } else if self.id == Some(id) { - if self.pressed_tab { + if self.pressed_tab && !self.is_focus_locked { self.id = None; self.give_to_next = true; 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.pressed_shift_tab = false; } @@ -302,11 +309,26 @@ impl Memory { 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. /// See also [`crate::Response::request_focus`]. #[inline(always)] pub fn request_focus(&mut self, id: Id) { self.interaction.focus.id = Some(id); + self.interaction.focus.is_focus_locked = false; } /// Surrender keyboard focus for a specific widget. @@ -315,6 +337,7 @@ impl Memory { pub fn surrender_focus(&mut self, id: Id) { if self.interaction.focus.id == Some(id) { self.interaction.focus.id = None; + self.interaction.focus.is_focus_locked = false; } } diff --git a/egui/src/ui.rs b/egui/src/ui.rs index e67a18f3c51..58c2ea0aa1a 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -934,6 +934,22 @@ impl Ui { 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() { … }` /// /// Shortcut for `add(Button::new(text))` diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index ed83debb669..5d8ee689d5b 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -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. /// /// See also [`Ui::text_edit_singleline`] and [`Ui::text_edit_multiline`]. @@ -140,6 +146,7 @@ pub struct TextEdit<'t> { enabled: bool, desired_width: Option, desired_height_rows: usize, + tab_moves_focus: bool, } impl<'t> TextEdit<'t> { pub fn cursor(ui: &Ui, id: Id) -> Option { @@ -171,6 +178,7 @@ impl<'t> TextEdit<'t> { enabled: true, desired_width: None, desired_height_rows: 1, + tab_moves_focus: true, } } @@ -189,9 +197,37 @@ impl<'t> TextEdit<'t> { enabled: true, desired_width: None, 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 { self.id = Some(id); self @@ -311,6 +347,7 @@ impl<'t> TextEdit<'t> { enabled, desired_width, desired_height_rows, + tab_moves_focus, } = self; let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style); @@ -416,6 +453,8 @@ impl<'t> TextEdit<'t> { let mut text_cursor = None; if ui.memory().has_focus(id) && enabled { + ui.memory().lock_focus(id, !tab_moves_focus); + let mut cursorp = state .cursorp .map(|cursorp| { @@ -465,12 +504,27 @@ impl<'t> TextEdit<'t> { && text_to_insert != "\r" { let mut ccursor = delete_selected(text, &cursorp); + insert_text(&mut ccursor, text, text_to_insert); Some(CCursorPair::one(ccursor)) } else { 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 { key: Key::Enter, pressed: true, diff --git a/egui_demo_lib/src/apps/demo/widgets.rs b/egui_demo_lib/src/apps/demo/widgets.rs index 620223d86db..244b142cdd9 100644 --- a/egui_demo_lib/src/apps/demo/widgets.rs +++ b/egui_demo_lib/src/apps/demo/widgets.rs @@ -22,8 +22,11 @@ pub struct Widgets { radio: Enum, angle: f32, color: Color32, + show_password: bool, single_line_text_input: String, multiline_text_input: String, + tab_moves_focus: bool, + code_snippet: String, } impl Default for Widgets { @@ -35,7 +38,31 @@ impl Default for Widgets { angle: std::f32::consts::TAU / 3.0, color: (Rgba::from_rgb(0.0, 1.0, 0.5) * 0.75).into(), 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(), + 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.separator(); + ui.label("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, + }, + ); } } diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index f053f9d296e..c55b2493d28 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -120,7 +120,7 @@ impl FontImpl { if c == '\t' { 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() { if !self.fonts.is_empty() { let (font_index, glyph_info) = self.glyph_info(c); + let font_impl = &self.fonts[font_index]; if let Some(last_glyph_id) = last_glyph_id { diff --git a/epaint/src/text/mod.rs b/epaint/src/text/mod.rs index 9706e47302d..fbd6eb6b0e7 100644 --- a/epaint/src/text/mod.rs +++ b/epaint/src/text/mod.rs @@ -5,6 +5,9 @@ mod font; mod fonts; mod galley; +/// Default size for a `\t` character. +pub const MAX_TAB_SIZE: f32 = 4.0; + pub use { fonts::{FontDefinitions, FontFamily, Fonts, TextStyle}, galley::{Galley, Row},