diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 6236151a2..6b2520adb 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -501,12 +501,16 @@ impl<'t> TextEdit<'t> { Event::Key { key: Key::Tab, pressed: true, - .. + modifiers, } => { if multiline && ui.memory().has_lock_focus(id) { let mut ccursor = delete_selected(text, &cursorp); - - insert_text(&mut ccursor, text, "\t"); + if modifiers.shift { + // TODO: support removing indentation over a selection? + decrease_identation(&mut ccursor, text); + } else { + insert_text(&mut ccursor, text, "\t"); + } Some(CCursorPair::one(ccursor)) } else { None @@ -1043,3 +1047,61 @@ fn next_word_boundary_char_index(it: impl Iterator, mut index: usiz fn is_word_char(c: char) -> bool { c.is_ascii_alphanumeric() || c == '_' } + +/// Accepts and returns character offset (NOT byte offset!). +fn find_line_start(text: &str, current_index: CCursor) -> CCursor { + // We know that new lines, '\n', are a single byte char, but we have to + // work with char offsets because before the new line there may be any + // number of multi byte chars. + // We need to know the char index to be able to correctly set the cursor + // later. + let chars_count = text.chars().count(); + + let position = text + .chars() + .rev() + .skip(chars_count - current_index.index) + .position(|x| x == '\n'); + + match position { + Some(pos) => CCursor::new(current_index.index - pos), + None => CCursor::new(0), + } +} + +fn decrease_identation(ccursor: &mut CCursor, text: &mut String) { + let mut new_text = String::with_capacity(text.len()); + + let line_start = find_line_start(text, *ccursor); + + let mut char_it = text.chars().peekable(); + for _ in 0..line_start.index { + let c = char_it.next().unwrap(); + new_text.push(c); + } + + let mut chars_removed = 0; + while let Some(&c) = char_it.peek() { + if c == '\t' { + char_it.next(); + chars_removed += 1; + break; + } else if c == ' ' { + char_it.next(); + chars_removed += 1; + if chars_removed == text::TAB_SIZE { + break; + } + } else { + break; + } + } + + new_text.extend(char_it); + + *text = new_text; + + if *ccursor != line_start { + *ccursor -= chars_removed; + } +} diff --git a/epaint/src/text/cursor.rs b/epaint/src/text/cursor.rs index 538de1f21..5403fd2ac 100644 --- a/epaint/src/text/cursor.rs +++ b/epaint/src/text/cursor.rs @@ -51,6 +51,18 @@ impl std::ops::Sub for CCursor { } } +impl std::ops::AddAssign for CCursor { + fn add_assign(&mut self, rhs: usize) { + self.index = self.index.saturating_add(rhs); + } +} + +impl std::ops::SubAssign for CCursor { + fn sub_assign(&mut self, rhs: usize) { + self.index = self.index.saturating_sub(rhs); + } +} + /// Row Cursor #[derive(Clone, Copy, Debug, Default, PartialEq)] #[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))] diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index 9cef1b032..3540f36f4 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -127,7 +127,7 @@ impl FontImpl { if c == '\t' { if let Some(space) = self.glyph_info(' ') { - glyph_info.advance_width = crate::text::TAB_SIZE * space.advance_width; + glyph_info.advance_width = crate::text::TAB_SIZE as f32 * space.advance_width; } } diff --git a/epaint/src/text/mod.rs b/epaint/src/text/mod.rs index 6f56ac2d0..9aab626ac 100644 --- a/epaint/src/text/mod.rs +++ b/epaint/src/text/mod.rs @@ -5,8 +5,8 @@ mod font; mod fonts; mod galley; -/// Default size for a `\t` character. -pub const TAB_SIZE: f32 = 4.0; +/// One `\t` character is this many spaces wide. +pub const TAB_SIZE: usize = 4; pub use { fonts::{FontDefinitions, FontFamily, Fonts, TextStyle},