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

Word wrapping in Multiline Text #3237

Open
pablojimenezmateo opened this issue May 16, 2020 · 18 comments
Open

Word wrapping in Multiline Text #3237

pablojimenezmateo opened this issue May 16, 2020 · 18 comments

Comments

@pablojimenezmateo
Copy link

pablojimenezmateo commented May 16, 2020

First of all, I am aware of the other posts regarding this issue: #952 #1062

This is a little bit different, I would like to attempt to implement this myself since this is not a high priority but I have a couple of questions.

Digging through the source code, I found that the editor is a slightly modified version of https://github.com/nothings/stb/blob/master/stb_textedit.h and, there is this particular paragraph commenting on word-wrapping:

// STB_TEXTEDIT_LAYOUTROW returns information about the shape of one displayed
// row of characters assuming they start on the i'th character--the width and
// the height and the number of characters consumed. This allows this library
// to traverse the entire layout incrementally. You need to compute word-wrapping
// here.

I see that that function is indeed implemented in imgui, more specifically in imgui_widgets.cpp:

static void    STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx)
{
    const ImWchar* text = obj->TextW.Data;
    const ImWchar* text_remaining = NULL;
    const ImVec2 size = InputTextCalcTextSizeW(text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true);
    r->x0 = 0.0f;
    r->x1 = size.x;
    r->baseline_y_delta = size.y;
    r->ymin = 0.0f;
    r->ymax = size.y;
    r->num_chars = (int)(text_remaining - (text + line_start_idx));

}

So if my understanding is correct, that function gets called when the cursor moves on the text (either by mouse or keyboard), but then I do not understand how that function would aid text wrapping.

That function does not get called when inserting new text, which should be were the wrapping should be checked.

So more specifically:

  • Does the function STB_TEXTEDIT_LAYOUTROW have anything to do with how text is rendered or is only used to move the cursor even in wrapped text?
  • What would be the overall steps to be able to achieve word wrapping? Is a total rewrite necessary or is it possible to achieve this by overriding a function?
  • I see that ImDrawList::AddText has a wrap_width parameter, I guess that if as a first step I should try to modify the calls to that function in ImGui::InputTextEx, is that correct?

Thank you for your understanding, I want to build a note taking application and, for me, this is a requirement so I would like this feature, if someone can help I would greatly appreciate that.

@ocornut
Copy link
Owner

ocornut commented May 16, 2020

Why not using the full featured text editor widgets listed in the wiki ?

@pablojimenezmateo
Copy link
Author

I have checked those projects and I do not require that much. Also adding this to the current implementation would help many developers such as myself.

I have been digging through the code and added the correct wrap_size to both AddText calls and the text does indeed get wrapped:

draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, inner_size.x, is_multiline ? NULL : &clip_rect);

But, the cursor is not updated correctly. I am trying to update the cursor myself when I detect that the text has been wrapped, I want to add a copy of ImFont::CalcWordWrapPositionA that would return the number of "wrapped lines" and update the cursor accordingly.

Although I have correctly ported the function (as static in the meantime) I am not 100% sure where the cursor is stored to update it.

I will try to dig around a little bit more, but any help will be welcomed :)

@pablojimenezmateo
Copy link
Author

I got some advances but the code is very complex and I did not have enough time to look at it over the weekend.

https://youtu.be/BBTho4ZnMIc

I have a better grasp on the complexity of the modifications:

  • stb_textedit only needs to be notified of were the cursor has to be moved
  • We need to keep track of the wrapped words and sizes for every lines, the rendering function is already done and moving the cursor is "trivial"
  • Then we need to keep track of mouse inputs and correctly translate the coordinates to "buffer" coordinates

@ocornut
Copy link
Owner

ocornut commented May 18, 2020

Looking good!

I haven't answered your questions above because I don't know what the answers are without digging deeper in that. My whole impression is that we should ditch stb_textedit and rewrite the whole thing anyway without the utf-8<>utf16 round trips.

You'll notice they are various shortcuts involved in InputText inner-loops, namely to allow for very large text and because we don't retain much data. If we rewrite we should probably try to retain more data, in particular line offsets, to make performances more acceptable.

@pablojimenezmateo
Copy link
Author

I have seen those inner loops indeed, but I am afraid I lack the expertise to improve the whole TextEdit code, there are many functions that are still unclear to me.

Still, I will try to implement the wrapping as it is something I need. Do you know if the TextEdit rewrite is still a low priority? I guess not a lot of people would benefit from it, but in my case it is crucial.

Also, thank you for answering, and for doing so so quickly!

@ocornut
Copy link
Owner

ocornut commented May 24, 2020

InputText rewrite is not a low priority but there are so many things competing for high priority at the moment it is a little tricky for me to try to even order them. There are many desirable InputText changes that would benefit from that larger refactor. I guess someone should sit down for 2 weeks and rewrite the whole thing from stratch with a check-list derived from entries in TODO.txt and inputtext tagged issues. That's not very helpful for you however.

I still believe the easiest path for you is to use one of the existing full-featured text editor.

@pablojimenezmateo
Copy link
Author

I understand, unfortunately I do not have the time nor the expertise for a full rewrite addressing all the open issues.

I wish I could contribute more, I just made a small donation in the hopes that this awesome project continues and some brave developer rewrites the TextEdit code :)

Thank you again for your time and your replies! I will try to use one of the existing text-editors in the meantime, as you suggested :)

@jenergy
Copy link

jenergy commented May 9, 2021

Imgui_1.79_multiline_wrap.zip
Here is my implementation. It's a patch from 1.79.
Basically, input/output buffer remains clean (= without fake \n), but display string (and bytes) are word wrapped where available, otherwise they are letter wrapped (= word is splitted). This means that some fake '\n' are added in display variables, but not in buffer.
Everytime buffer IS GOING TO BE updated:

  • Display string and bytes are reconstructed from OLD buffer WITHOUT fake \n
  • Normal edit is performed on display bytes (so buffer will be updated..)
  • Display string and bytes are recomputed from NEW buffer WITH fake \n.
    Easy to say, not so easy to implement..

Notes:

  1. I Added two parameters to method InputTextEx:
  • a boolean indicating word_wrapping is requested
  • a (pointer to a) boolean indicating user wants to 'initialize' state. For example, if you have your multiline wrapped textarea in a secondary window, every time window is shown you may want to move scrollbar at minimum position (and other initialization stuff you may need)
  1. I used STATIC implementation and I did not tested callbacks. Maybe extra code is needed if you need callbacks
  2. Performance seems to be good, but I didn't tested a VERY LONG text. Maybe an improvement can be a 'partial' reconstruction of the string instead of the whole string
  3. I added some variable state to the ImGuiInputTextState structure. But this is shared with other textInput components. So if you have more than one multiline wrap inputText in your window, maybe you would have a kind of hashMap having child ID as key, so that every multiline component can have its own set of these variables
  4. I tested it on Windows

@timprepscius
Copy link

I took the time to isolate jenergy's patch:
https://github.com/timprepscius/imgui/tree/multiline_wrap_jenergy

I tried to merge the jenergy with current-master, but it doesn't seem to work (compiles, but weirdness):
https://github.com/timprepscius/imgui/tree/tjp_main_multiline_editing

I then wrote a patch that seems to work.
https://github.com/ocornut/imgui/compare/master...timprepscius:imgui:tjp_multiline?expand=1

Obviously very new, will be working on bit by bit if bugs arise.

Take if you want.

@jenergy
Copy link

jenergy commented Aug 16, 2022 via email

@abvadabra
Copy link

abvadabra commented Feb 17, 2023

Just to add to the discussion, here is my relatively old patch for wordwrapping support in multiline textfield, based on 1.83. It is relatively stable (there are some minor quirks but nothing serious) and we've been using it in our editors for two years now.

It reuses ImDrawList::AddText which supports word wrapping, with some additional logic to handle navigation in wrapped text.

It consists of two commits, but can be easily combined into one:
abvadabra@d6be798
abvadabra@4fb8f2f

video.3.mp4

@dgm3333
Copy link

dgm3333 commented Jun 20, 2023

I'd totally vote for this being included in the core code - as lack of workwrap in multiline edit is sad (although I also sympathise with ocornut's comment that everyone has "just one more" thing so I get why it isn't there...

Anyway just to add my contribution - purely because it doesn't require modifying core ImGui but takes advantage of the multiline edit.
This is a very quick NON PRODUCTION hack which adds soft returns.

To use it:
At initialisation run the addSoftReturnsToText on the string.
If multilineWidth changes remove all softreturns then rerun addSoftReturnsToText to put them back in new locations.
When the editing is finished then remove all "\r\n" to get the core string back with edits.

imgui_autosizingMultilineInput isn't required - I use it to ensure sufficient height for the text without -1 making it eat up all the space (and some others bits which aren't required here)


void addSoftReturnsToText(std::string& str, float multilineWidth) {

	float textSize = 0;
	std::string tmpStr = "";
	std::string finalStr = "";
	int curChr = 0;
	while (curChr < str.size()) {

		if (str[curChr] == '\n') {
			finalStr += tmpStr + "\n";
			tmpStr = "";
		}

		tmpStr += str[curChr];
		textSize = ImGui::CalcTextSize(tmpStr.c_str()).x;

		if (textSize > multilineWidth) {
			int lastSpace = tmpStr.size() - 1;
			while (tmpStr[lastSpace] != ' ' and lastSpace > 0)
				lastSpace--;
			if (lastSpace == 0)
				lastSpace = tmpStr.size() - 2;
			finalStr += tmpStr.substr(0, lastSpace+1) + "\r\n";
			if (lastSpace + 1 > tmpStr.size())
				tmpStr = "";
			else
				tmpStr = tmpStr.substr(lastSpace+1);
		}
		curChr++;
	}
	if (tmpStr.size() > 0)
		finalStr += tmpStr;

	str = finalStr;
};


bool imgui_autosizingMultilineInput(globalSettings& gS, const char* label, std::string* str, const ImVec2& sizeMin, const ImVec2& sizeMax, ImGuiInputTextFlags flags = ImGuiInputTextFlags_None) {

	// calculate the maximum y/height
	ImGui::PushTextWrapPos(sizeMax.x);
	auto textSize = ImGui::CalcTextSize(str->c_str());
	if (textSize.x > sizeMax.x) {
		float ratio = textSize.x / sizeMax.x;
		textSize.x = sizeMax.x;
		textSize.y *= ratio;
		textSize.y += 20;		// add space for an extra line
	}

	textSize.y += 8;		// to compensate for inputbox margins

	if (textSize.x < sizeMin.x)
		textSize.x = sizeMin.x;
	if (textSize.y < sizeMin.y)
		textSize.y = sizeMin.y;
	if (textSize.x > sizeMax.x)
		textSize.x = sizeMax.x;
	if (textSize.y > sizeMax.y)
		textSize.y = sizeMax.y;

	bool value_changed = ImGui::InputTextMultiline(label, str, textSize, flags);
	ImGui::PopTextWrapPos();

	return value_changed;
}



@pixtur
Copy link

pixtur commented Jan 16, 2024

It would be quite handy to have this a as a standard component.
Since I'm on imgui.net the full text editor components are sadly not easy to include.

@sodamouse
Copy link

Giving a +1 to this issue.

@ocornut
Copy link
Owner

ocornut commented Feb 27, 2024

It is too complex to do efficiently with current code for InputText but i’m hoping to rewrite this sometimes this year.

@themistocles-0
Copy link

themistocles-0 commented Sep 4, 2024

I have seen this issue being raised since 2017. I am in desperate need of line wrapping in the multi-line text input. Has there been any progress in its implementation?

jstncno pushed a commit to jstncno/imgui that referenced this issue Nov 19, 2024
This is an inefficient, experimental word-wrapping implementation.
For more details, see: ocornut#3237 (comment)
@ocornut
Copy link
Owner

ocornut commented Nov 20, 2024

@seltzdesign Your contribution is not constructive neither helpful. Nobody argued against this. Feel free to contribute the PR that solves this with the performance characteristics expected by Dear ImGui when handling e.g. large buffers. It has been a long time coming but we made stride of progress both in term of InputText redesign and in terms of reworking text functions so I am confident this should be solved in 2025. We happens to also have a thousands of other things to do or it just hasn't been a huge priority.

@seltzdesign
Copy link

@ocornut you're right, sorry, not the right place for discussion. Deleted the post..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants