giu
giu copied to clipboard
[Feature Request] InputTextMultiline() needs text wrapping, for example via Wrapped(true) like labels.
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
well, I always tought that there is a flag for it, but it isn't... I'll think about some alternative too.
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
}
@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.
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).
@rasteric thanks for your code! it saved me now :smile: 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