Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tab identation for multiline text edit #246

Merged
merged 42 commits into from
May 2, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
86c4b76
Add multiline TextEdit to the demo
DrOptix Mar 21, 2021
2f806a8
Lock focus on multiline text edit
DrOptix Mar 22, 2021
94c1801
When TAB is detected add 4 Space chars
DrOptix Mar 23, 2021
ba12f9e
Implement tabs as spaces
DrOptix Mar 23, 2021
13ed86d
Sync with work done upstream
DrOptix Mar 23, 2021
19e2608
Make the "tab-as-character" behaviour opt-in.
DrOptix Mar 24, 2021
b3aa498
Adapt the URL open command to the platform
DrOptix Mar 24, 2021
5557b17
Add code editor shortuct functions
DrOptix Mar 24, 2021
cb66aaa
Apply partial suggestions from code review
DrOptix Mar 24, 2021
1d271d5
Remove identation using `shift`+`tab`.
DrOptix Mar 27, 2021
b15a5c7
Single line identation removal fix + tests
DrOptix Mar 27, 2021
724901e
Fix indentation insert in tab as spaces mode
DrOptix Mar 27, 2021
f2ddd8f
Fix tab rendering
DrOptix Mar 28, 2021
4633084
WIP: Rework identation management
DrOptix Mar 28, 2021
5e6fc2d
Refactor: start from clean slate
DrOptix Mar 28, 2021
aafcc03
Refactor: handle identation insert at cursor
DrOptix Mar 29, 2021
d7f3805
Refactor: replace selection in paragraph with identation
DrOptix Mar 29, 2021
939733b
Refactor: Decrease identation
DrOptix Mar 31, 2021
4186356
Refactor: keep selection offsets
DrOptix Apr 1, 2021
b386599
Additional identation management
DrOptix Apr 1, 2021
5bc7857
Fix selection update in tabs-as-tabs mode
DrOptix Apr 1, 2021
7065e4e
Merge branch 'feature/73_identation_text_edit_widget_refactor' into f…
DrOptix Apr 1, 2021
798748d
cargo fmt
DrOptix Apr 1, 2021
c81132f
Sync with upstream
DrOptix Apr 1, 2021
da2392c
Sync with upstream
DrOptix Apr 1, 2021
9e6db0f
Fix build: add missing tab_glyph_info_cache
DrOptix Apr 1, 2021
30fcc1e
Fix identation management in tabs-as-tabs mode
DrOptix Apr 1, 2021
0220a04
Add Fedora command to install wasm-strip
DrOptix Apr 1, 2021
18429f0
Implement identation deletion with backspace
DrOptix Apr 2, 2021
1272ad9
Rework identation management
DrOptix Apr 2, 2021
d468dc7
Use `CodingConfig` to customize code editor
DrOptix Apr 2, 2021
061b56b
Sync with upstream
DrOptix Apr 2, 2021
e14c549
Sync with upstream
DrOptix Apr 2, 2021
04b8cfb
Remove `ccursor_paragraph_end` as it is no longer used
DrOptix Apr 2, 2021
78ac783
Sync with upstream
DrOptix Apr 3, 2021
1b3b5a4
Merge branch 'master' into feature/73_identation_text_edit_widget
DrOptix Apr 6, 2021
4396c87
Sync with upstream
DrOptix Apr 17, 2021
ae4ea45
Sync with upstream
DrOptix Apr 17, 2021
f384003
Sync with upstream
DrOptix Apr 26, 2021
0fe4cce
Sync with upstream
DrOptix Apr 26, 2021
c57a147
Cleanup according to PR reviews
DrOptix Apr 26, 2021
de7a474
Tabs are always rendered as 4 spaces no matter where they are
DrOptix Apr 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion build_demo_web.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ wasm-bindgen "target/wasm32-unknown-unknown/$BUILD/$TARGET_NAME" \

echo "Finished: docs/${CRATE_NAME}.wasm"

open http://localhost:8888/index.html
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
# Linux, ex: Fedora
xdg-open http://localhost:8888/index.html
elif [[ "$OSTYPE" == "msys" ]]; then
# Windows
start http://localhost:8888/index.html
else
# Darmin aka MacOS or something else
DrOptix marked this conversation as resolved.
Show resolved Hide resolved
open http://localhost:8888/index.html
fi
DrOptix marked this conversation as resolved.
Show resolved Hide resolved
35 changes: 29 additions & 6 deletions egui/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,12 @@ pub(crate) struct Focus {
/// The last widget interested in focus.
last_interested: Option<Id>,

/// Set at the beginning of the frame, set to `false` when "used".
DrOptix marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -186,6 +190,7 @@ impl Focus {
}
) {
self.id = None;
self.is_focus_locked = false;
break;
}

Expand All @@ -195,10 +200,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 @@ -225,11 +232,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 @@ -287,14 +294,30 @@ 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
pub fn request_focus(&mut self, id: Id) {
self.interaction.focus.id = Some(id);
self.interaction.focus.is_focus_locked = false;
}

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
19 changes: 19 additions & 0 deletions egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,25 @@ impl Ui {
self.add(TextEdit::multiline(text))
}

/// A `TextEdit` for code editing.
///
/// Se also [`TextEdit::code_editor`].
DrOptix marked this conversation as resolved.
Show resolved Hide resolved
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,
tab_as_spaces: bool,
tab_moves_focus: bool,
) -> Response {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is bad form to have multiple boolean arguments, as it leads to code that is very hard to decipher:

TextEdit::code_editor_with_config(code, true, false) // what does it mean!?

I think it is better in this case to just have the opinionated code_editor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.add(TextEdit::multiline(text).code_editor_with_config(tab_as_spaces, tab_moves_focus))
}

/// Usage: `if ui.button("Click me").clicked() { … }`
///
/// Shortcut for `add(Button::new(text))`
Expand Down
92 changes: 92 additions & 0 deletions egui/src/widgets/text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ pub struct TextEdit<'t> {
enabled: bool,
desired_width: Option<f32>,
desired_height_rows: usize,
tab_as_spaces: bool,
tab_moves_focus: bool,
}
impl<'t> TextEdit<'t> {
pub fn cursor(ui: &Ui, id: Id) -> Option<CursorPair> {
Expand Down Expand Up @@ -165,6 +167,8 @@ impl<'t> TextEdit<'t> {
enabled: true,
desired_width: None,
desired_height_rows: 1,
tab_as_spaces: false,
tab_moves_focus: true,
}
}

Expand All @@ -182,9 +186,71 @@ impl<'t> TextEdit<'t> {
enabled: true,
desired_width: None,
desired_height_rows: 4,
tab_as_spaces: false,
tab_moves_focus: true,
}
}

/// Registers if this widget will insert spaces instead of tab char
///
/// ```rust, ignore
/// ui.add(egui::TextEdit::multiline(&mut self.multiline_text_input)
/// .tab_as_spaces(true));
/// ```
pub fn tab_as_spaces(mut self, b: bool) -> Self {
emilk marked this conversation as resolved.
Show resolved Hide resolved
self.tab_as_spaces = b;
self
}

/// When this is true, then pass focus to the next
/// widget.
///
/// When this is false, then insert identation based on the value of
/// `tab_as_spaces` property.
///
/// ```rust, ignore
/// ui.add(egui::TextEdit::multiline(&mut self.multiline_text_input)
/// .tab_moves_focus(true));
/// ```
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
/// - tab as spaces
///
/// Shortcut for:
/// ```rust, ignore
/// egui::TextEdit::multiline(code_snippet)
/// .text_style(TextStyle::Monospace)
/// .tab_as_spaces(true)
/// .tab_moves_focus(false);
/// ```
pub fn code_editor(self) -> Self {
self.text_style(TextStyle::Monospace)
.tab_as_spaces(true)
.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_as_spaces(tab_as_spaces)
/// .tab_moves_focus(tab_moves_focus);
/// ```
pub fn code_editor_with_config(self, tab_as_spaces: bool, tab_moves_focus: bool) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here about multiple boolean arguments. If you want to set multiple values with a single call, it is better to make a config struct and pass that in (struct CodeConfig { tabs_as_spaces: bool, tab_width: usize, … } ). This has the benefit of being more explicit and more extensible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.code_editor()
.tab_as_spaces(tab_as_spaces)
.tab_moves_focus(tab_moves_focus)
}

pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
Expand Down Expand Up @@ -297,6 +363,8 @@ impl<'t> TextEdit<'t> {
enabled,
desired_width,
desired_height_rows,
tab_as_spaces,
tab_moves_focus,
} = self;

let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
Expand Down Expand Up @@ -383,6 +451,8 @@ impl<'t> TextEdit<'t> {
}

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 @@ -432,12 +502,34 @@ 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 {
let mut ccursor = delete_selected(text, &cursorp);

if ui.memory().has_lock_focus(id) {
if tab_as_spaces {
insert_text(&mut ccursor, text, " ");
} else {
insert_text(&mut ccursor, text, "\t");
}
}
emilk marked this conversation as resolved.
Show resolved Hide resolved

Some(CCursorPair::one(ccursor))
} else {
None
}
}
Event::Key {
key: Key::Enter,
pressed: true,
Expand Down
40 changes: 39 additions & 1 deletion egui_demo_lib/src/apps/demo/widgets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ pub struct Widgets {
angle: f32,
color: Color32,
single_line_text_input: String,
tab_size: usize,
DrOptix marked this conversation as resolved.
Show resolved Hide resolved
tab_as_spaces: bool,
tab_moves_focus: bool,
multiline_text_input: String,
code_snippet: String,
}

impl Default for Widgets {
Expand All @@ -35,7 +39,22 @@ 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(),
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(),

tab_size: 4,
tab_as_spaces: true,
tab_moves_focus: false,
multiline_text_input: r#"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.
This is the start of the next paragraph.

Existing tabs are kept as tabs.
DrOptix marked this conversation as resolved.
Show resolved Hide resolved

Click me to edit me!"#.to_owned(),
code_snippet: r#"
// Use the configs above to play with the tab management
fn main() {
println!("Hello world!");
}
"#.to_owned(),
}
}
}
Expand Down Expand Up @@ -133,7 +152,26 @@ impl Widgets {
}
});

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_as_spaces, "Tabs as spaces");
ui.checkbox(&mut self.tab_moves_focus, "Tabs moves focus");
DrOptix marked this conversation as resolved.
Show resolved Hide resolved
});

ui.code_editor_with_config(
&mut self.code_snippet,
self.tab_as_spaces,
self.tab_moves_focus,
);
}
}