-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Tab identation for multiline text edit #246
Conversation
When we load a string in `TextEdit` we keep the tabs as they are. Now depending on `tab_as_spaces` and `tab_size` properties of the `TextEdit` we have the following situations: - When `tab_as_spaces` is false, then when we press `TAB` a `TAB` char in added to the string. - When `tab_as_spaces` is true, then when we press `TAB` a chunck of `tab_size` spaces is added to the string. To config `tab_as_spaces` and `tab_size` to the values we want we can use the folliwing helper functions: - `TextEdit::tab_as_spaces()` - `TextEdit::tab_size()` Example usage: ```rust ui.add(egui::TextEdit::multiline(&mut self.multiline_text_input) .tab_as_spaces(self.tab_as_spaces) .tab_size(self.tab_size)); ``` Demo: A better demo of this new functionality was implemented in the misc widgets demo What's left to do: - Render actual `TAB` as a rect the size of `tab_size` spaces. - Without default parameters values it seems a pain to implement. I need to think if it is possible to avoid API breaking changes for the font system. From my checks so far I need to pass at least `tab_size` to the fonts system to know how big the glyph for `TAB` will be.
Added `tab_moves_focus()` to `TextEdit` so we can opt-in to the "tab-as-character" behaviour. Also because we decided to lock the tab size to 4 spaces, we gave up on the `tab_size` property, but we kept the opt-in for the "tab-as-spaces" behaviour. An updated example of how to use `TextEdit` with those two opt-in behaviours can be found in the "Misc Demos" demo.
With this new version we use $OSTYPE bash env var to query what OS are we running on. - On Linux, ex: Fedora, we use `xdg-open` - On Windows, ex: msys, we use `start` - For other other variants we try to use `open` We should update this script when we notice that `open` is not available on a particular platform.
For both `Ui` and `TextEdit` add two helper functions to simplify creation on `TextEdit` focused on code editing. - `code_editor` - `code_editor_with_config`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work!
egui/src/ui.rs
Outdated
text: &mut String, | ||
tab_as_spaces: bool, | ||
tab_moves_focus: bool, | ||
) -> Response { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I crated a CodingConfig
struct for this purpose.
https://github.com/DrOptix/egui/blob/e14c5491b61574647ed837130e0a61f4c1ce98b4/egui/src/widgets/text_edit.rs#L107-L112
egui/src/widgets/text_edit.rs
Outdated
/// .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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I crated a CodingConfig
struct for this purpose.
https://github.com/DrOptix/egui/blob/e14c5491b61574647ed837130e0a61f4c1ce98b4/egui/src/widgets/text_edit.rs#L107-L112
Thank you @emilk for the suggestions Co-authored-by: Emil Ernerfeldt <[email protected]>
Depending on the type of identation we first convert the current identation in to spaces or tabs. For converting to spaces we have two cases: 1. The identation converts to a number of spaces, multiple of the tab size. In this case we remove a number of spaces equal to the tab size. 2. The identation converts to a number of space, not a multiple of tab size. In this case we remove spaces until we have a number of spaces multiple of tab size. For converting to tabs we have two cases: 1. The identation converts only to tabs. In this case we remove a tab char. 2. The identation converts to tabs, and 1, 2 or 3 spaces. In this case we remove the spaces
When inserting the identation first we check on which virtual column we are and then add enough spaces to accound for an identation size of `text::MAX_TAB_SIZE`. Above we say virtual column because a `tab` can occupy 1, 2, 3 or 4 columns and we need to keep in mind this.
If we have a single cursor on a paragraph then we will decrease identation only on that paragraph. If we have a selection we will decrease identation on all selected paragraphs.
- `Tab`: When multiple lines are selected add identation - `Ctrl`+`[`: Remove identation on single or multiple lines - `Ctrl`+`]`: Add identation on single or multiple lines
…eature/73_identation_text_edit_widget With this merge the identation management was reworked. Before adding or removing identation we convert the identation of the targeted lines to tabs or spaces depending on how we treat the tabs. The selection will be maintained. Now we can do this: - When we are in single cursor mode and we press `tab` and we will insert a `\t` or the needed number of spaces to complete a block according or `text::MAX_TAB_SIZE`. - Add identation to multiple selected lines using `tab`. - Use `shit`+`tab` to remove identation from the current line or the selected lines. - Use `ctrl`+`[` and `ctrl`+`]` to remove or add identation to the current line or a group of selected lines.
Oh wow, this kind of got out of hand didn't it :) It's amazing with a code editor in egui, but I'm a worried about adding 1550 lines of code. The whole I won't have time to review this for another few days, but anything you can think of the reduce the complexity would be appreciated. In particular: do we really need to supports spaces for indentation? Making an editor that only supports tabs would make everything A LOT simpler (just add or remove the first tab on a line). If a user wants spaces in their code when they save to disk, then can just convert to/from spaces on load/save instead. The code to convert to/from tabs/spaces would also be a lot simpler as we wouldn't need to track the cursor position. Or, going the other direction, if we want a full-fledged code editor for egui, maybe that should be a separate crate ( |
Well yes, I went from ecstasy to frustration and back to ecstasy. I will try to think of a way to reduce the complexity. For me at least the biggest complexity was represented by keeping track of remaining tabulation size. I used VS Code as a blueprint for how to add, remove or convert indentation and also how to keep the selection while adding or removing indentation. |
To explain my last post a bit more: I consider all code to be a liability and a burden. I always strive to express as much as possible with as little as possible. Each line of code is something that needs to be maintained in perpetuity, therefor each line of code must pull its weight. In this particular case I know that the cursor handling in egui will need to be rewritten in the future, probably several times, to support things like right-to-left languages and moving over graphemes rather than characters. This PR will make such work a lot harder. If an advanced code editor was a separate crate, then egui can be refactored and the code editor crate can follow, instead of doing both in one swoop (and by the same person). But in this case I think we can get 90% of the result with 10% of the code by only supporting tabs for indentation, and force users to convert their code to that convention if they want to use egui as a code editor. As stated before, it is rather simple to convert to/from tabs when loading said code, so there is really no need for egui to support both. PS: |
TL;DR: IMHO this should all be in a separate crate except for the part where you can disable tab moving focus and swap over to tab inserting Nice work! However, AFAICT this has some major flaws:
Overall, although I like the idea, all the stuff aside from not moving focus on tab should at the very least be separated into a separate PR. Further, IMHO, the code here is too big to be wieldy while also being too "basic" for many use cases anyway. Both of these factors make it unfit for the core library and as such it should really be just a separate crate. egui is a GUI library. It should expose just enough features so the user can build whatever they want to using it. It shouldn't try to build the GUI for the user; that's still the user's job (and for good reason, as it's unreasonable for one library to support every opinionated use case). |
Hello sorry for the inactivity, I was focusing on something else. I will try to clean the PR this weekend, but my questions is, what should I leave in it? Now about configurable key bindings, at the moment all key bindings are hard coded and from what I know there is no KeyMapManager available that fires events when some configurable key binding was detected. I'm OK with both giving up on this PR or cleaning it and leave only what's needed and/or acceptable. Thank you a lot for your feedback! |
Right, which is why the code editor should be implemented in user code. Along with the user's own key map manager. I'm not emilk, but I think this PR should implement, as you say, only the part where you can capture the focus and insert \t and rendering it as 4 spaces. However, definitely don't get rid of your current code! You've done a lot of great work and it could be used as a starting point when implementing a code editor in the future on top of egui, probably as an external crate. |
And in any case, even if we decide to add a "code editor" to core egui sometime, that doesn't need to happen in this very PR. Those two basic changes mentioned above would be enough to fix #73. If we want to discuss this more and maybe merge more of your changes, you can always open a new PR. |
I think that would be good, yes. I think the main bulk of this PR could be turned into an |
Cleanup according to PR reviews
The only thing left is the tab rendering which adaps to the remaining We consider the following sequence of characters, separated with
It will be rendered like this, using ---> to denote the space occupied
|
- Removed tab_as_spaces idea. - Removed identation management The only thing left is the tab rendering which adaps to the remaining space inside the current tab block. We consider the following sequence of characters, separated with spaces for clarity: ``` \t 1 \t 12 \t 123 \t 1234 \t ``` It will be rendered like this, using ---> to denote the space occupied by a tab: ``` 1234 | 1234 | 1234 | 1234 | 1234 -----+------+------+------+----- ---> | 1--> | 12-> | 123> | ---> ```
I don't really understand the adaptation thing. I don't seem to be able to replicate your example in Google Docs nor OSX Pages, could you clarify what this is actually supposed to be doing? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do the stupid thing of always keeping tabs at width=4. Tabs should only be used for indentation anyway ("indent with tabs, align with spaces")
epaint/src/text/font.rs
Outdated
for c in text.chars() { | ||
if !self.fonts.is_empty() { | ||
let (font_index, glyph_info) = self.glyph_info(c); | ||
// For tabs it is nice to let them occupy only what's left of the current |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is very common behavior, but is it "nice"? Does anyone actually like this behavior? It adds a lot of complication for a dubious feature.
If we should do something smart with tabs, we should implement elastic tabstops instead (but not in this PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still wrapping my head around it, but it looks really interesting
epaint/src/text/font.rs
Outdated
} else { | ||
tab_size -= 1; | ||
self.glyph_info(c) | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot simpler implementation of this would be to just modify glyph_info.advance_width
directly here. No need for fn tab_glyph_info
and a separate glyph cache for different tab widths
All the simplifications should be done now. |
With this PR we resolve #73, allowing opt-in tab indentation in
TextEdit
When we load a string in
TextEdit
we keep the tabs as they are and by defaulttab_as_space
is initialized tofalse
andtab_moves_focus
is initialized totrue
.To config
tab_as_spaces
andtab_moves_focus
to the values we want we can use the following helper functions:TextEdit::tab_as_spaces()
TextEdit::tab_moves_focus()
Example:
Now depending on
tab_as_spaces
andtab_moves_focus
properties of theTextEdit
we have the following situations:When
tab_as_spaces
is false, then when we pressTAB
the result is aTAB
char is added to the string with the width of 4 spaces.When
tab_as_spaces
is true, then when we pressTAB
the result is a chunk of 4 spaces are added to the string.To fully understand how to use
TextEdit
with those two opt-in behaviors check the "Misc Demos" demo.Other two helper functions were added to
Ui
andTextEdit
to simplify the creation of aTextEdit
focused on code editing:code_editor
code_editor_with_config
The following key combinations are supported. Depending on the "tab-as-spaces" config the indentation block will be firstly converted to the appropriate type and the increased or decreased.
tab
Insert
\t
or needed number of spaces to align to the tabulation size.\t
char will be rendered so that it will align to the tabulation sizeshift
+tab
Will remove indentation at the end of the indentation block to align the text to the tabulation size. This works on the current line or with multiple lines selected.
shift
+[
andshift
+]
Remove and add indentation at the end of the indentation block to align the text to the tabulation size.
Another improvement was done to
build_demo_web.sh
script to adapt the "open" command to the platform we work on. This is not covering all possible platforms, but it's a good template to have for when we need to adapt to a new platform:xdg-open
for Linux, Fedora for examplestart
for Windows, MSYSopen
for anything else