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

[Feature Request] InputTextMultiline() needs text wrapping, for example via Wrapped(true) like labels. #434

Open
rasteric opened this issue Jan 19, 2022 · 13 comments
Labels
documentation Improvements or additions to documentation enhancement New feature or request

Comments

@rasteric
Copy link

Related problem

Currently, InputTextMultiline() does not seem to be able to wrap text (unless I've missed something). To automatically wrap text is pretty much standard for such widgets nowadays. Is this a limitation of imgui?

Your request

InputTextMultiline().Wrapped(true) should make a multiline text entry to soft-wrap text on unicode whitespace characters.

Alternative solution

I'm trying to come up with a manual wrapping solution using g.CalcTextSize(s) but haven't gotten far yet. It's kind of complicated because of the editing, it shouldn't force the user to delete hard linefeeds when removing text.

Additional context

No response

@rasteric rasteric added the enhancement New feature or request label Jan 19, 2022
@gucio321
Copy link
Collaborator

well, I always tought that there is a flag for it, but it isn't...
I'll think about some alternative too.

@rasteric
Copy link
Author

rasteric commented Jan 20, 2022

I've found a crazy workaround that illustrates the fantastic flexibility of Go and this framework. First I define the word wrap callback:

// the word wrap callback parameterized for a multiline text input
cb := func(widget *g.InputTextMultilineWidget, data imgui.InputTextCallbackData) int32 {
	c := data.CursorPos()
	if c <= 0 {
		return 0
	}
	buff := data.Buffer()
	var nl int
	for nl = c - 1; nl > 0; nl-- {
		if buff[nl] == 10 {
			break
		}
	}
	w := g.GetWidgetWidth(widget)
	if TextWidth(string(buff[nl:c])) > w {
		for i := c - 1; i > nl; i-- {
			if buff[i] == 32 {
				buff[i] = 10
				data.MarkBufferModified()
				break
			}
		}
	}
	return 0
}

Now define the widget in advance instead of within the layout, and use the closure for defining its callback:

// prepare multiline input widgets and their word wrap callbacks
infoWidget := g.InputTextMultiline(&a.layoutBuff.info).Size(-1, 100).
	Flags(imgui.InputTextFlagsCallbackAlways)
cbInfo := func(data imgui.InputTextCallbackData) int32 {
	return cb(infoWidget, data)
}

Later, I render it like this: infoWidget.Callback(cbInfo)

The hack seems to work, although I've probably have missed some edge cases. It's hard-wrapping instead of soft-wrapping, but that's okay for my use case.

Helper function:

// TextWidth returns the width of the given text.
func TextWidth(s string) float32 {
	w, _ := g.CalcTextSize(s)
	return w
}

@gucio321
Copy link
Collaborator

gucio321 commented Feb 2, 2022

@rasteric I'd suggest u to check/ask in https://github.com/ocornut/imgui if they have (or are planning to have) this feature, because IMO this feature should be in base repo instead implemented here.

@rasteric
Copy link
Author

rasteric commented May 4, 2022

Quick update, in case someone is interested. I found a workaround. It's hard because imgui and therefore GIU does not render any other newline-like unicode character as a new paragraph, only NEWLINE character 13. Therefore, it's not easy to distinguish between user-entered newline (persistent) and new lines introduced by the word breaking algorithms. Here is how I managed to do it nevertheless:

// WrapInputTextMultiline is a callback to wrap an input text multiline.
func WrapInputtextMultiline(widget *g.InputTextMultilineWidget, data imgui.InputTextCallbackData) int32 {
	switch data.EventFlag() {
	case imgui.InputTextFlagsCallbackCharFilter:
		c := data.EventChar()
		if c == '\n' {
			data.SetEventChar('\u07FF') // pivot character 2-bytes in UTF-8
		}

	case imgui.InputTextFlagsCallbackAlways:
		// 0. turn every pivot byte sequence into \r\n
		buff := data.Buffer()
		buff2 := []byte(strings.ReplaceAll(string(buff), "\u07FF", "\r\n"))
		for i := range buff {
			buff[i] = buff2[i]
		}
		data.MarkBufferModified()

		// 1. zap all newlines that are not preceeded by a CR (which was manually entered like above)
		cr := false
		for i, c := range buff {
			if c == 10 && !cr {
				buff[i] = 32
				data.MarkBufferModified()
			} else {
				if c == 13 {
					cr = true
				} else {
					cr = false
				}
			}
		}
		// 2. word break the whole buffer with the standard greedy algorithm
		nl := 0
		spc := 0
		w := g.GetWidgetWidth(widget)
		for i, c := range buff {
			if c == 10 {
				nl = i
			}
			if c == 32 {
				spc = i
			}
			if TextWidth(string(buff[nl:i])) > w && spc > 0 {
				buff[spc] = 10
				data.MarkBufferModified()
			}
		}
	}
	return 0
}

This needs to be wrapped into the callback, e.g. like this:

previewInfoWidget := g.InputTextMultiline(&fields.assetNote).Size(-1, -1).
		Flags(imgui.InputTextFlagsCallbackAlways | imgui.InputTextFlagsCallbackCharFilter)
previewInfo := func(data imgui.InputTextCallbackData) int32 {
		return WrapInputtextMultiline(previewInfoWidget, data)
	}
// possibly do something else, and later in the render loop:
previewInfoWidget.Callback(previewInfo)

You need to set both InputTextFlagsCallbackAlways and InputTextFlagsCallbackCharFilter.

How it works: I first introduce some pivot char of exactly 2 bytes in UTF-8, I chose the first one \u07FF. Then, we replace these 2 bytes in the buffer with the sequence \r\n. We cannot to this in SetEventChar because it only allows one rune! The \n in the sequence \r\n is rendered as newline, whereas the \r is invisible (not a space). We then can zap all \n that are not part of a sequence \r\n and apply the greedy word breaking algorithm on each callback in CallbackAlways.

The downside is that deleting user-input newline requires pressing backspace 2 times, which could be fixed by overriding the backspace functionality. It's barely tested and only on Linux so far. I feel like I've hacked into a Gibson (just kidding).

@gucio321
Copy link
Collaborator

@rasteric thanks for your code! it saved me now 😄
here is my small modification:

func WrapInputtextMultiline(widget *giu.InputTextMultilineWidget, data imgui.InputTextCallbackData) int32 {
	switch data.EventFlag() {
	case imgui.InputTextFlagsCallbackCharFilter:
		c := data.EventChar()
		if c == '\n' {
			data.SetEventChar('\u07FF') // pivot character 2-bytes in UTF-8
		}

	case imgui.InputTextFlagsCallbackAlways:
		// 0. turn every pivot byte sequence into \r\n
		buff := data.Buffer()
		buff2 := []byte(strings.ReplaceAll(string(buff), "\u07FF", "\r\n"))
		for i := range buff {
			buff[i] = buff2[i]
		}
		data.MarkBufferModified()

		// 1. zap all newlines that are not preceeded by a CR (which was manually entered like above)
		cr := false
		for i, c := range buff {
			if c == 10 && !cr {
				buff[i] = 32
				data.MarkBufferModified()
			} else {
				if c == 13 {
					cr = true
				} else {
					cr = false
				}
			}
		}
		// 2. word break the whole buffer with the standard greedy algorithm
		nl := 0
		spc := 0
		w := giu.GetWidgetWidth(widget)
		for i, c := range buff {
			if c == 10 {
				nl = i
			}
			if c == 32 {
				spc = i
			}
			if TextWidth(string(buff[nl:i])) > w {
				if spc > 0 {
					buff[spc] = 10
				} else {
					data.InsertBytes(len(buff)-1, []byte{10})
				}
				data.MarkBufferModified()
			}
		}
	}
	return 0
}

I was manibulating a veeeeery long strings, and the string had to be force-splited if no spaces found

@gucio321 gucio321 added the documentation Improvements or additions to documentation label Jun 4, 2023
@featherL
Copy link

featherL commented Jun 5, 2024

@rasteric thanks for your code! it saved me now 😄 here is my small modification:

func WrapInputtextMultiline(widget *giu.InputTextMultilineWidget, data imgui.InputTextCallbackData) int32 {
	switch data.EventFlag() {
	case imgui.InputTextFlagsCallbackCharFilter:
		c := data.EventChar()
		if c == '\n' {
			data.SetEventChar('\u07FF') // pivot character 2-bytes in UTF-8
		}

	case imgui.InputTextFlagsCallbackAlways:
		// 0. turn every pivot byte sequence into \r\n
		buff := data.Buffer()
		buff2 := []byte(strings.ReplaceAll(string(buff), "\u07FF", "\r\n"))
		for i := range buff {
			buff[i] = buff2[i]
		}
		data.MarkBufferModified()

		// 1. zap all newlines that are not preceeded by a CR (which was manually entered like above)
		cr := false
		for i, c := range buff {
			if c == 10 && !cr {
				buff[i] = 32
				data.MarkBufferModified()
			} else {
				if c == 13 {
					cr = true
				} else {
					cr = false
				}
			}
		}
		// 2. word break the whole buffer with the standard greedy algorithm
		nl := 0
		spc := 0
		w := giu.GetWidgetWidth(widget)
		for i, c := range buff {
			if c == 10 {
				nl = i
			}
			if c == 32 {
				spc = i
			}
			if TextWidth(string(buff[nl:i])) > w {
				if spc > 0 {
					buff[spc] = 10
				} else {
					data.InsertBytes(len(buff)-1, []byte{10})
				}
				data.MarkBufferModified()
			}
		}
	}
	return 0
}

I was manibulating a veeeeery long strings, and the string had to be force-splited if no spaces found

I used the code to show a copyable and auto-wrap text(very long),but something wrong as follow

Assertion failed: Buf == edit_state->TextA.Data, file imgui_widgets.cpp, line 3650

I guess error occurs in data.InsertBytes(len(buff)-1, []byte{10}). Did i misuse this code?

my code:

// github.com/AllenDang/giu v0.7.0

// WrapInputtextMultiline

func CopyableText(s *string) giu.Widget {
	ret := giu.InputTextMultiline(s).
		Flags(giu.InputTextFlagsReadOnly | giu.InputTextFlagsCallbackAlways | giu.InputTextFlagsCallbackCharFilter)
	cb := func(data imgui.InputTextCallbackData) int32 {
		return WrapInputtextMultiline(ret, data)
	}
	ret.Callback(cb)

	return ret
}

var tmp = strings.Repeat("a", 1000)

func loop() {
	giu.SingleWindow().Layout(
		CopyableText(&tmp),
	)
}

func main() {
	wnd := giu.NewMasterWindow("wechat viewer", 800, 600, 0)
	wnd.Run(loop)
}

@gucio321
Copy link
Collaborator

gucio321 commented Jun 5, 2024

@featherL after debugging, I found out whats the problem. If you remove all SetBuffer* calls,
panic changes and it says !read_only.
You can't use InputTextFlagsReadOnly because then your callback can't edit content

@gucio321
Copy link
Collaborator

gucio321 commented Jun 5, 2024

btw @featherL could you post ap-to-date version of WrapInputMultiline? I think that one above was for imgui-go version. And updated version'd be extremely useful ;-)

@featherL
Copy link

featherL commented Jun 6, 2024

btw @featherL could you post ap-to-date version of WrapInputMultiline? I think that one above was for imgui-go version. And updated version'd be extremely useful ;-)

Thanks. But I haven't solved this problem yet. And The WrapInputtextMultiline callbak will add too many '\n' if I removed the flag InputTextFlagsReadOnly.

@gucio321
Copy link
Collaborator

gucio321 commented Jun 6, 2024

turns out that (*InputTextCallbackData).SetBuf(...) is no longer a valid thing to do.
According to imgui code:

                    IM_ASSERT(callback_data.Buf == callback_buf);         // Invalid to modify those fields

@gucio321
Copy link
Collaborator

gucio321 commented Jun 6, 2024

ok, @featherL I managed to fix this, here is the code compatible with latest giu:

// WrapInputtextMultiline is a community-crafted function that allows to add auto-wrap to
// InputTextMultilineWidget in giu. It is a workaround for the lack of this feature in Dear ImGui.
// Credits:
// - @rasteric the original poster
// - @gucio321 modified for cimgui-go compatibility
// see: https://github.com/AllenDang/giu/issues/434
//
// [@gucio321] I try to figure out this code.
// ref:
// - ascii table: https://www.asciitable.com/
func WrapInputtextMultiline(widget *giu.InputTextMultilineWidget, data imgui.InputTextCallbackData) int {
	const (
		SPACE   = ' '
		CR      = '\r'
		NEWLINE = '\n'
	)

	switch data.EventFlag() {
	// [@gucio321] This is expected to catch user-defined newlines and mark them with the following character
	// to prevent it from being removed/modified in the code below.
	case imgui.InputTextFlagsCallbackCharFilter:
		c := data.EventChar()
		if c == '\n' {
			data.SetEventChar('\u07FF') // pivot character 2-bytes in UTF-8
		}

	case imgui.InputTextFlagsCallbackAlways:
		// 0. turn every pivot byte sequence into \r\n
		buff := []byte(data.Buf())
		buff = []byte(strings.ReplaceAll(string(buff), "\u07FF", "\r\n"))

		// remove all auto-added newlines
		cr := false
		for i, c := range buff {
			switch c {
			case CR:
				cr = true // preserve \n following this \r
			case NEWLINE:
				if cr {
					cr = false
					break
				}

				buff[i] = SPACE
			}
		}

		// 2. word break the whole buffer with the standard greedy algorithm
		nl := 0
		spc := 0
		w := giu.GetWidgetWidth(widget)
		for i, c := range buff {
			switch c {
			case NEWLINE:
				nl = i
			case SPACE:
				spc = i
			}

			if TextWidth(string(buff[nl:i])) <= w {
				continue
			}

			// this happens when the line should be wrapped
			if spc > 0 {
				buff[spc] = NEWLINE
			} else {
				data.InsertChars(int32(len(buff)-1), string([]byte{10}))
			}
		}

		data.SetBufDirty(true)

		// [@gucio321] since data.SetBuf is no longer a valid thing to be done since newer Dear ImGui,
		// [@gucio321] we need to do this in this fancy way
		data.DeleteChars(0, int32(len(buff)))
		data.InsertChars(0, string(buff))
	}
	return 0
}

@gucio321
Copy link
Collaborator

gucio321 commented Jun 6, 2024

@AllenDang should we add this as a built-in feature of InputTextMultilneWidget?

Personally, I think this should remain "this magic code that you can copy from this issue on our github" as it is bugged as hell and not really oficial and supported by imgui.

what do you think?

@AllenDang
Copy link
Owner

@gucio321 Agree to leave it here as an example of how to implement wrapped in InputTextMultiline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants