Skip to content

Commit

Permalink
Tab identation for multiline text edit (#246)
Browse files Browse the repository at this point in the history
Lock focus on multiline text edit, and insert tabs on tab char

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
DrOptix and emilk authored May 2, 2021
1 parent 101eed0 commit 35c7b09
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 7 deletions.
35 changes: 29 additions & 6 deletions egui/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,12 @@ pub(crate) struct Focus {
/// The last widget interested in focus.
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".
pressed_tab: bool,

/// Set at the beginning of the frame, set to `false` when "used".
pressed_shift_tab: bool,
}
Expand Down Expand Up @@ -199,6 +203,7 @@ impl Focus {
}
) {
self.id = None;
self.is_focus_locked = false;
break;
}

Expand All @@ -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;
}
}
}
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}
}

Expand Down
16 changes: 16 additions & 0 deletions egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))`
Expand Down
54 changes: 54 additions & 0 deletions egui/src/widgets/text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down Expand Up @@ -140,6 +146,7 @@ pub struct TextEdit<'t> {
enabled: bool,
desired_width: Option<f32>,
desired_height_rows: usize,
tab_moves_focus: bool,
}
impl<'t> TextEdit<'t> {
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
Expand Down Expand Up @@ -171,6 +178,7 @@ impl<'t> TextEdit<'t> {
enabled: true,
desired_width: None,
desired_height_rows: 1,
tab_moves_focus: true,
}
}

Expand All @@ -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
Expand Down Expand Up @@ -312,6 +348,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);
Expand Down Expand Up @@ -417,6 +454,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| {
Expand Down Expand Up @@ -466,12 +505,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,
Expand Down
46 changes: 46 additions & 0 deletions egui_demo_lib/src/apps/demo/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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,
},
);
}
}
3 changes: 2 additions & 1 deletion epaint/src/text/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions epaint/src/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down

0 comments on commit 35c7b09

Please sign in to comment.