ebitenui / ebitenui Goto Github PK
View Code? Open in Web Editor NEWUser interface engine and widget library for Ebiten
Home Page: https://ebitenui.github.io
License: MIT License
User interface engine and widget library for Ebiten
Home Page: https://ebitenui.github.io
License: MIT License
Let's say I have a 800x600 window/screen and I'm rendering on layers and on the last layer I render a small rectangle image middle of the screen, we could think about it as a modal dialog. If I render the UI inside this image by calling ui.Draw(image) where the image is just a copy of this small rectangle then this small rectangle is drawn to the screen.
When buttons are hovered nothing happens but if you but the cursor in the place where the buttons will get rendered over the whole screen I do see the buttons changing colors.
Have done some debugging and cursor x, y position are indeed inside the Rect {x, y, widht, height} of the buttons/widgets, what I have tried was to set the ui.container Rect value to be the size of this, small rectangle image, even calling relayout after this but the children rects does not change their values.
At the end it would be nice, I'm not sure if this is already available but have an offset.x offset.y of the entire screen on where Container child Rects will use to detect these events.
If you create a text input widget and set its text value, selecting (focusing) the input would create a caret near the 1st character.
textinput.InputText = "some data"
This behavior can be worked around with a new DoGoEnd()
method, which could be a recommended way to solve this issue.
Let's think about it and come up with a recommended approach to handle it.
For now, we have focus change directions of next/previous.
This is good enough for horizontal or vertical layouts like these:
vertical: next is right, prev is left
[x][x][x][x]
horizontal: next is down, prev is up
[x]
[x]
[x]
[x]
It becomes less intuitive with grid-like layouts (inventory, achievements grid, etc.):
grid layout: can only traverse it in a linear one after another way, from the upper left to the bottom right
[x][x][x]
[x][x][x]
[x][x][x]
I propose that we add an alternative focus-changing scheme with directions (4 of them).
For the already existing vertical and horizontal behaviors the change is trivial:
For the grid layout we can now have a better navigation system.
It could be possible to implement this in the user-land, but it will be messy, since we'll have to take the layout into account while doing the computations.
Without this feature, any keyboard/gamepad based navigation over the grid becomes clumsy and inconvenient. In turn, it forces the game developers that are using this library to avoid any grid setups to mitigate the limitations.
My personal opinion is that directional buttons should not go out of the layout easily. When you press left on the horizontal layout while focusing the leftmost element, it should wrap around and select the rightmost element, not some other random widget that happened to be somewhere. We need a separate action for that like "select next group" which could be assigned to tab or whatever. If layout-based solution is not good enough here, groups can be (and maybe they should be) marked by the user explicitly.
First off, really cool UI project! The demo looks great!
I was playing around with the various widgets and encountered a crash when dragging outside of the application window. See screenshot below (right before the crash).
Stack trace is attached below (at rev 466226f).
$ _demo
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x673226]
goroutine 8 [running]:
main.(*dragContents).Update(0xc00005cc40, 0x0, 0x0, 0x161, 0x15e, 0x0, 0x0)
/tmp/ebitenui/_demo/dnd.go:38 +0x26
github.com/blizzy78/ebitenui/widget.(*DragAndDrop).draggingState.func1(0xc00019b080, 0x753690, 0x0, 0x0)
/tmp/ebitenui/widget/dnd.go:180 +0x3c9
github.com/blizzy78/ebitenui/widget.(*DragAndDrop).Render(0xc000069c70, 0xc00019b080, 0x753690)
/tmp/ebitenui/widget/dnd.go:113 +0x3e
github.com/blizzy78/ebitenui/widget.renderDeferredRenderQueue(0xc00019b080)
/tmp/ebitenui/widget/widget.go:359 +0x84
github.com/blizzy78/ebitenui/widget.RenderWithDeferred(0xc00019b080, 0xc0002461b0, 0x3, 0x3)
/tmp/ebitenui/widget/widget.go:347 +0x109
github.com/blizzy78/ebitenui.(*UI).render(0xc00015dc20, 0xc00019b080)
/tmp/ebitenui/ui.go:140 +0x2a6
github.com/blizzy78/ebitenui.(*UI).Draw(0xc00015dc20, 0xc00019b080)
/tmp/ebitenui/ui.go:64 +0x21f
main.(*game).Draw(0xc000010d00, 0xc00019b080)
/tmp/ebitenui/_demo/main.go:376 +0x38
github.com/hajimehoshi/ebiten/v2.(*imageDumperGameWithDraw).Draw(0xc000a8a420, 0xc00019b080)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/run.go:120 +0x48
github.com/hajimehoshi/ebiten/v2.(*uiContext).draw(0xaa0640)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/uicontext.go:214 +0x6e
github.com/hajimehoshi/ebiten/v2.(*uiContext).Draw(0xaa0640, 0x0, 0x0)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/uicontext.go:174 +0xb1
github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).update(0x9271a0, 0x20001, 0x1)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/internal/uidriver/glfw/ui.go:871 +0x30d
github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).loop(0x9271a0, 0x0, 0x0)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/internal/uidriver/glfw/ui.go:914 +0xcf
github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).run(0x9271a0, 0x0, 0x0)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/internal/uidriver/glfw/ui.go:769 +0x2bd
github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).Run.func1(0x9271a0, 0xc000a6cae0)
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/internal/uidriver/glfw/ui.go:602 +0x72
created by github.com/hajimehoshi/ebiten/v2/internal/uidriver/glfw.(*UserInterface).Run
/home/u/goget/pkg/mod/github.com/hajimehoshi/ebiten/[email protected]/internal/uidriver/glfw/ui.go:594 +0x12f
If there is not enough space in the right part of the screen, the tooltip should probably position itself to the left, so it fits the screen.
If UI elements are positioned to the right side of the screen, it would be impossible to read a tooltip if it tries to render itself to the right side as well.
Doing input.SetCursorUpdater(nil)
makes the game panic.
internalinput.InputHandler
is internal.
There should be a way to redo the set operation.
Perhaps SetCursorUpdater
should check for nil and set the updater back to the default state if it is, in fact, nil?
Since SetCursorUpdater
will not be called in a loop, this extra check is ~free for us in terms of performance.
This is useful in games that don't use ebitenui in all of their scenes. Or if they have different parts of the game, some of them may benefit from system defaults (like no ebitenui cursor skins are used).
It looks like the demo example specifies the hover sprite for the TextInput widget.
That sprite is never used.
Looking at TextInputImage
, there is no Hover image option there.
Maybe it's not implemented yet?
I'm opening this issue as a reminder.
If there should be no hover variant for the text input, this issue can be closed, and "hover" sprite can be removed from the demo example to avoid any confusion.
and how to show line numbers?
Right now CursorUpdater
can't be used for virtual cursors that co-exist with normal cursor.
In my game, there is a separate cursor for the gamepad that doesn't remove a normal mouse pointer.
Its sprite is controlled outside of the ebitenui; I move it according to the player's inputs.
The game knows when either of them is active. So it would make more sense to me to make a logic-only cursor updater: I want ebitenui to know the cursor coordinates and whether some buttons were pressed, but the image-related APIs are redundant in this case.
It can be solved in at least two ways:
This is probably the minimal interface that would work for me:
type CursorUpdater interface {
MouseButtonPressed(b ebiten.MouseButton) bool
MouseButtonJustPressed(b ebiten.MouseButton) bool
CursorPosition() (int, int)
GetCursorOffset(name string) image.Point
}
This is a bridge from the game own logic to how ebitenui should handle cursor-related info.
Sorry if this is a silly question, but what's the recommended way of just having a generic "canvas" (ie ebiten.image) that can be put inside a container and draw to?
Requesting solution or support for vector-based option to replace the remaining PNGs. (Combobox and Checkbox)
I have gone through and reworked the demo resources.go file to be able to use a theme with the colors defined at the top. (I may move this to a config file later.) Now, I am to the part where I have a handful of images left that I cannot seem to replace. So, I would like to request either (1) Support for SVG, as Egitengine does support Vector graphics or (2) some other option that can be used in place of the loadGraphicImages
for the widget.CheckboxGraphicImage
properties and the comboButtonResources.graphic
.
I would mostly be interested in a way to supply these theme colors and have the shapes render with the assigned colors (thus one big reason for the interest in the vector graphics.)
This function is provided for quick reference.
func loadGraphicImages(idle string, disabled string) (*widget.ButtonImageImage, error) {
idleImage, err := newImageFromFile(idle)
if err != nil {
return nil, err
}
var disabledImage *ebiten.Image
if disabled != "" {
disabledImage, err = newImageFromFile(disabled)
if err != nil {
return nil, err
}
}
return &widget.ButtonImageImage{
Idle: idleImage,
Disabled: disabledImage,
}, nil
}
I have put comments next to the properties I was able to to figure out. There are ?
next to ones I could not.
package main
import (
"image/color"
"strconv"
"github.com/ebitenui/ebitenui/image"
"github.com/ebitenui/ebitenui/widget"
"golang.org/x/image/font"
)
/*
Fresca:
primaryColor = color.NRGBA{R: 0x07, G: 0x68, B: 0x9F, A: 0xff}
secondaryColor = color.NRGBA{R: 0xC9, G: 0xD6, B: 0xDF, A: 0xff}
successColor = color.NRGBA{R: 0x11, G: 0xD3, B: 0xBC, A: 0xff}
dangerColor = color.NRGBA{R: 0xF6, G: 0x72, B: 0x80, A: 0xff}
infoColor = color.NRGBA{R: 0xA2, G: 0xD5, B: 0xF2, A: 0xff}
warningColor = color.NRGBA{R: 0xFF, G: 0x7E, B: 0x67, A: 0xff}
lightColor = color.NRGBA{R: 0xFA, G: 0xFA, B: 0xFA, A: 0xff}
darkColor = color.NRGBA{R: 0x4E, G: 0x4E, B: 0x4E, A: 0xff}
Greyson:
primaryColor = color.NRGBA{R: 0x2f, G: 0x3c, B: 0x48, A: 0xff}
secondaryColor = color.NRGBA{R: 0x6f, G: 0x7f, B: 0x8c, A: 0xff}
successColor = color.NRGBA{R: 0x3e, G: 0x4d, B: 0x59, A: 0xff}
dangerColor = color.NRGBA{R: 0xcc, G: 0x33, B: 0x0d, A: 0xff}
infoColor = color.NRGBA{R: 0x5c, G: 0x8f, B: 0x94, A: 0xff}
warningColor = color.NRGBA{R: 0x6e, G: 0x9f, B: 0xa5, A: 0xff}
lightColor = color.NRGBA{R: 0xec, G: 0xee, B: 0xec, A: 0xff}
darkColor = color.NRGBA{R: 0x1e, G: 0x2b, B: 0x37, A: 0xff}
Hootstrap:
primaryColor = color.NRGBA{R: 0x52, G: 0x80, B: 0x78, A: 0xff}
secondaryColor = color.NRGBA{R: 0xee, G: 0xd7, B: 0x5a, A: 0xff}
successColor = color.NRGBA{R: 0xFE, G: 0xC1, B: 0x00, A: 0xff}
dangerColor = color.NRGBA{R: 0x70, G: 0x3B, B: 0x3B, A: 0xff}
infoColor = color.NRGBA{R: 0x63, G: 0xe7, B: 0x92, A: 0xff}
warningColor = color.NRGBA{R: 0xFF, G: 0xE8, B: 0x69, A: 0xff}
lightColor = color.NRGBA{R: 0xFD, G: 0xFB, B: 0xF7, A: 0xff}
darkColor = color.NRGBA{R: 0x55, G: 0x55, B: 0x55, A: 0xff}
*/
var (
primaryColor = color.NRGBA{R: 0x2f, G: 0x3c, B: 0x48, A: 0xff}
secondaryColor = color.NRGBA{R: 0x6f, G: 0x7f, B: 0x8c, A: 0xff}
successColor = color.NRGBA{R: 0x3e, G: 0x4d, B: 0x59, A: 0xff}
dangerColor = color.NRGBA{R: 0xcc, G: 0x33, B: 0x0d, A: 0xff}
infoColor = color.NRGBA{R: 0x5c, G: 0x8f, B: 0x94, A: 0xff}
warningColor = color.NRGBA{R: 0x6e, G: 0x9f, B: 0xa5, A: 0xff}
lightColor = color.NRGBA{R: 0xec, G: 0xee, B: 0xec, A: 0xff}
darkColor = color.NRGBA{R: 0x1e, G: 0x2b, B: 0x37, A: 0xff}
primarySelectedColor = darkenColor(primaryColor, 0.2)
secondarySelectedColor = darkenColor(secondaryColor, 0.2)
primaryHoverColor = lightenColor(primaryColor, 0.2)
secondaryHoverColor = lightenColor(secondaryColor, 0.2)
lightOffsetColor = darkenColor(lightColor, 0.03)
primaryLightColor = lightenColor(primaryColor, 0.70)
primaryHoverLightColor = lightenColor(primaryColor, 0.50)
primaryColor9 = image.NewNineSliceColor(primaryColor)
secondaryColor9 = image.NewNineSliceColor(secondaryColor)
successColor9 = image.NewNineSliceColor(successColor)
dangerColor9 = image.NewNineSliceColor(dangerColor)
infoColor9 = image.NewNineSliceColor(infoColor)
warningColor9 = image.NewNineSliceColor(warningColor)
lightColor9 = image.NewNineSliceColor(lightColor)
darkColor9 = image.NewNineSliceColor(darkColor)
primarySelectedColor9 = image.NewNineSliceColor(primarySelectedColor)
secondarySelectedColor9 = image.NewNineSliceColor(secondarySelectedColor)
primaryHoverColor9 = image.NewNineSliceColor(primaryHoverColor)
secondaryHoverColor9 = image.NewNineSliceColor(secondaryHoverColor)
lightOffsetColor9 = image.NewNineSliceColor(lightOffsetColor)
primaryLightColor9 = image.NewNineSliceColor(primaryLightColor)
primaryHoverLightColor9 = image.NewNineSliceColor(primaryHoverColor)
)
func darkenColor(c color.NRGBA, percent float64) color.NRGBA {
r := uint8(float64(c.R) * (1 - percent))
g := uint8(float64(c.G) * (1 - percent))
b := uint8(float64(c.B) * (1 - percent))
return color.NRGBA{R: r, G: g, B: b, A: c.A}
}
func lightenColor(c color.NRGBA, percent float64) color.NRGBA {
r := uint8(float64(c.R) + (float64(0xff-c.R) * percent))
g := uint8(float64(c.G) + (float64(0xff-c.G) * percent))
b := uint8(float64(c.B) + (float64(0xff-c.B) * percent))
return color.NRGBA{R: r, G: g, B: b, A: c.A}
}
type uiResources struct {
fonts *fonts
background *image.NineSlice
separatorColor color.Color
text *textResources
button *buttonResources
label *labelResources
checkbox *checkboxResources
comboButton *comboButtonResources
list *listResources
slider *sliderResources
progressBar *progressBarResources
panel *panelResources
tabBook *tabBookResources
header *headerResources
textInput *textInputResources
textArea *textAreaResources
toolTip *toolTipResources
}
type textResources struct {
idleColor color.Color
disabledColor color.Color
face font.Face
titleFace font.Face
bigTitleFace font.Face
smallFace font.Face
}
type buttonResources struct {
image *widget.ButtonImage
text *widget.ButtonTextColor
face font.Face
padding widget.Insets
}
type checkboxResources struct {
image *widget.ButtonImage
graphic *widget.CheckboxGraphicImage
spacing int
}
type labelResources struct {
text *widget.LabelColor
face font.Face
}
type comboButtonResources struct {
image *widget.ButtonImage
text *widget.ButtonTextColor
face font.Face
graphic *widget.ButtonImageImage
padding widget.Insets
}
type listResources struct {
image *widget.ScrollContainerImage
track *widget.SliderTrackImage
trackPadding widget.Insets
handle *widget.ButtonImage
handleSize int
face font.Face
entry *widget.ListEntryColor
entryPadding widget.Insets
}
type sliderResources struct {
trackImage *widget.SliderTrackImage
handle *widget.ButtonImage
handleSize int
}
type progressBarResources struct {
trackImage *widget.ProgressBarImage
fillImage *widget.ProgressBarImage
}
type panelResources struct {
image *image.NineSlice
titleBar *image.NineSlice
padding widget.Insets
}
type tabBookResources struct {
buttonFace font.Face
buttonText *widget.ButtonTextColor
buttonPadding widget.Insets
}
type headerResources struct {
background *image.NineSlice
padding widget.Insets
face font.Face
color color.Color
}
type textInputResources struct {
image *widget.TextInputImage
padding widget.Insets
face font.Face
color *widget.TextInputColor
}
type textAreaResources struct {
image *widget.ScrollContainerImage
track *widget.SliderTrackImage
trackPadding widget.Insets
handle *widget.ButtonImage
handleSize int
face font.Face
entryPadding widget.Insets
}
type toolTipResources struct {
background *image.NineSlice
padding widget.Insets
face font.Face
color color.Color
}
func newUIResources() (*uiResources, error) {
// background := lightColor
fonts, err := loadFonts()
if err != nil {
return nil, err
}
button, err := newButtonResources(fonts)
if err != nil {
return nil, err
}
checkbox, err := newCheckboxResources()
if err != nil {
return nil, err
}
comboButton, err := newComboButtonResources(fonts)
if err != nil {
return nil, err
}
list, err := newListResources(fonts)
if err != nil {
return nil, err
}
slider, err := newSliderResources()
if err != nil {
return nil, err
}
progressBar, err := newProgressBarResources()
if err != nil {
return nil, err
}
panel, err := newPanelResources()
if err != nil {
return nil, err
}
tabBook, err := newTabBookResources(fonts)
if err != nil {
return nil, err
}
header, err := newHeaderResources(fonts)
if err != nil {
return nil, err
}
textInput, err := newTextInputResources(fonts)
if err != nil {
return nil, err
}
textArea, err := newTextAreaResources(fonts)
if err != nil {
return nil, err
}
toolTip, err := newToolTipResources(fonts)
if err != nil {
return nil, err
}
return &uiResources{
fonts: fonts,
background: lightColor9,
separatorColor: secondaryColor,
text: &textResources{
idleColor: primaryColor,
disabledColor: darkColor,
face: fonts.face,
titleFace: fonts.titleFace,
bigTitleFace: fonts.bigTitleFace,
smallFace: fonts.toolTipFace,
},
button: button,
label: newLabelResources(fonts),
checkbox: checkbox,
comboButton: comboButton,
list: list,
slider: slider,
panel: panel,
tabBook: tabBook,
header: header,
textInput: textInput,
toolTip: toolTip,
textArea: textArea,
progressBar: progressBar,
}, nil
}
func newButtonResources(fonts *fonts) (*buttonResources, error) {
i := &widget.ButtonImage{
Idle: primaryColor9, // button background
Hover: primaryHoverColor9, // button hover background
Pressed: infoColor9, // active selected button background
PressedHover: primaryLightColor9, // button click background
Disabled: secondaryColor9,
}
return &buttonResources{
image: i,
text: &widget.ButtonTextColor{
Idle: lightColor, // button text color
Disabled: primaryLightColor,
},
face: fonts.face,
padding: widget.Insets{
Left: 30,
Right: 30,
},
}, nil
}
func newCheckboxResources() (*checkboxResources, error) {
// TODO: Replace images
checked, err := loadGraphicImages("assets/graphics/checkbox-checked-idle.png", "assets/graphics/checkbox-checked-disabled.png")
if err != nil {
return nil, err
}
unchecked, err := loadGraphicImages("assets/graphics/checkbox-unchecked-idle.png", "assets/graphics/checkbox-unchecked-disabled.png")
if err != nil {
return nil, err
}
greyed, err := loadGraphicImages("assets/graphics/checkbox-greyed-idle.png", "assets/graphics/checkbox-greyed-disabled.png")
if err != nil {
return nil, err
}
// checkbox toggle switch button
return &checkboxResources{
image: &widget.ButtonImage{
Idle: lightColor9, // checkbox main background
Hover: primaryLightColor9, // checkbox hover background
Pressed: lightColor9, // checkbox click background
Disabled: secondaryColor9,
},
graphic: &widget.CheckboxGraphicImage{
Checked: checked, // TODO: Replace
Unchecked: unchecked, // TODO: Replace
Greyed: greyed, // TODO: Replace
},
spacing: 10,
}, nil
}
func newLabelResources(fonts *fonts) *labelResources {
return &labelResources{
text: &widget.LabelColor{
Idle: primaryColor,
Disabled: secondaryColor,
},
face: fonts.face,
}
}
// Select input dropdown
func newComboButtonResources(fonts *fonts) (*comboButtonResources, error) {
i := &widget.ButtonImage{
Idle: lightColor9, // main select input background
Hover: primaryLightColor9, // main select input hover
Pressed: lightColor9, // main select input click
Disabled: secondaryColor9,
}
// TODO: Replace images
arrowDown, err := loadGraphicImages("assets/graphics/fast-arrow-down.png", "assets/graphics/fast-arrow-down.png")
if err != nil {
return nil, err
}
return &comboButtonResources{
image: i,
text: &widget.ButtonTextColor{
Idle: primaryColor,
Disabled: secondaryColor,
},
face: fonts.face,
graphic: arrowDown, // TODO: replace
padding: widget.Insets{
Left: 20,
Right: 10,
},
}, nil
}
func newListResources(fonts *fonts) (*listResources, error) {
return &listResources{
image: &widget.ScrollContainerImage{ // scroll bar container
Idle: lightColor9,
Disabled: secondaryColor9,
Mask: dangerColor9, // not sure what this is yet
},
track: &widget.SliderTrackImage{ // The scroll bar
Idle: secondaryColor9,
Hover: secondaryHoverColor9,
Disabled: secondarySelectedColor9,
},
trackPadding: widget.Insets{
Top: 2, // padding of the handle on the bar
Bottom: 2,
},
handle: &widget.ButtonImage{
Idle: primarySelectedColor9, // color of idle handle
Hover: secondaryHoverColor9, // scroll bar handle hover
Pressed: primarySelectedColor9, // scroll bar handle click
Disabled: secondaryColor9,
},
handleSize: 5,
face: fonts.face,
entry: &widget.ListEntryColor{
Unselected: primaryColor,
DisabledUnselected: secondaryColor,
Selected: lightColor, // selected and active of text
DisabledSelected: secondarySelectedColor,
SelectedBackground: successColor, // seclected and active of box
DisabledSelectedBackground: darkColor,
FocusedBackground: primaryLightColor, // box of hover
SelectedFocusedBackground: primarySelectedColor,
},
entryPadding: widget.Insets{
Left: 30,
Right: 30,
Top: 2,
Bottom: 2,
},
}, nil
}
func newSliderResources() (*sliderResources, error) {
return &sliderResources{
trackImage: &widget.SliderTrackImage{
Idle: lightColor9,
Hover: secondaryHoverColor9,
Disabled: secondaryColor9,
},
handle: &widget.ButtonImage{
Idle: primaryColor9,
Hover: primaryHoverColor9,
Pressed: primarySelectedColor9,
Disabled: secondaryColor9,
},
handleSize: 6,
}, nil
}
func newProgressBarResources() (*progressBarResources, error) {
return &progressBarResources{
trackImage: &widget.ProgressBarImage{
Idle: lightColor9, // Progress bar empty background
Hover: primaryHoverColor9, // ?
Disabled: secondaryColor9, // ?
},
fillImage: &widget.ProgressBarImage{
Idle: primaryLightColor9, // bar of progress bar
Hover: primaryHoverLightColor9, // ?
Disabled: secondaryColor9, // ?
},
}, nil
}
func newPanelResources() (*panelResources, error) {
return &panelResources{
image: lightOffsetColor9, // background of panel
titleBar: lightColor9, // ?
padding: widget.Insets{
Left: 30,
Right: 30,
Top: 20,
Bottom: 20,
},
}, nil
}
func newTabBookResources(fonts *fonts) (*tabBookResources, error) {
return &tabBookResources{
buttonFace: fonts.face,
buttonText: &widget.ButtonTextColor{
Idle: lightColor, // tab button text
Disabled: darkColor, // disabled tab button text
},
buttonPadding: widget.Insets{
Left: 30,
Right: 30,
},
}, nil
}
func newHeaderResources(fonts *fonts) (*headerResources, error) {
return &headerResources{
background: lightColor9,
padding: widget.Insets{
Left: 25,
Right: 25,
Top: 4,
Bottom: 4,
},
face: fonts.bigTitleFace,
color: primaryColor,
}, nil
}
func newTextInputResources(fonts *fonts) (*textInputResources, error) {
return &textInputResources{
image: &widget.TextInputImage{
Idle: lightColor9, // input box background
Disabled: lightOffsetColor9, // disabled input box background
},
padding: widget.Insets{
Left: 8,
Right: 8,
Top: 4,
Bottom: 4,
},
face: fonts.face,
color: &widget.TextInputColor{
Idle: primaryColor, // input box text input color
Disabled: secondaryColor, // disabled input box text input color
Caret: primaryLightColor, // input box cursor color
DisabledCaret: secondarySelectedColor, // disabled input box cursor color
},
}, nil
}
func newTextAreaResources(fonts *fonts) (*textAreaResources, error) {
return &textAreaResources{
image: &widget.ScrollContainerImage{
Idle: primaryColor9,
Disabled: secondaryColor9,
Mask: primaryHoverColor9,
},
track: &widget.SliderTrackImage{
Idle: primaryColor9,
Hover: primaryHoverColor9,
Disabled: secondaryColor9,
},
trackPadding: widget.Insets{
Top: 5,
Bottom: 24,
},
handle: &widget.ButtonImage{
Idle: lightColor9,
Hover: secondaryHoverColor9,
Pressed: secondarySelectedColor9,
Disabled: primarySelectedColor9,
},
handleSize: 5,
face: fonts.face,
entryPadding: widget.Insets{
Left: 30,
Right: 30,
Top: 2,
Bottom: 2,
},
}, nil
}
func newToolTipResources(fonts *fonts) (*toolTipResources, error) {
return &toolTipResources{
background: lightColor9,
padding: widget.Insets{
Left: 15,
Right: 15,
Top: 10,
Bottom: 10,
},
face: fonts.toolTipFace,
color: primaryColor,
}, nil
}
func (u *uiResources) close() {
u.fonts.close()
}
func hexToColor(h string) color.Color {
u, err := strconv.ParseUint(h, 16, 0)
if err != nil {
panic(err)
}
return color.NRGBA{
R: uint8(u & 0xff0000 >> 16),
G: uint8(u & 0xff00 >> 8),
B: uint8(u & 0xff),
A: 255,
}
}
Hi!
I'm building a chat for my game using ebitenui and I'm not sure if it's possible to handle the Enter
event on a TextInput
.
The ChangedEvent
doesn't seem to trigger on Enter in any way, only on content updates.
Already using the text area to display a chat like UI, which is great!!! There is one use case that I don't see covered, what if I want to have an Editable Text Area, let's say that I want to allow users to send a feedback message, a small UI to set your email and a text area where users can type to send the message, we do have Input Text, but it does not feel like the right UI for things like this.
I tried searching for Visible
or Hidden
properties but found none.
Is there a way to hide a widget?
I see a way to disable a widget, but sometimes you want a different state: disabled+invisible (or we can imply that invisible elements are disabled automatically).
--- a/_examples/widget_demos/button/main.go
+++ b/_examples/widget_demos/button/main.go
@@ -7,10 +7,8 @@ import (
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/image"
"github.com/ebitenui/ebitenui/widget"
- "github.com/golang/freetype/truetype"
+ "github.com/hajimehoshi/bitmapfont/v2"
"github.com/hajimehoshi/ebiten/v2"
- "golang.org/x/image/font"
- "golang.org/x/image/font/gofont/goregular"
)
// Game object used by ebiten
@@ -22,8 +20,7 @@ func main() {
// load images for button states: idle, hover, and pressed
buttonImage, _ := loadButtonImage()
- // load button text font
- face, _ := loadFont(20)
+ face := bitmapfont.Face
// construct a new container that serves as the root of the UI hierarchy
rootContainer := widget.NewContainer(
@@ -49,7 +46,7 @@ func main() {
widget.ButtonOpts.Image(buttonImage),
// specify the button's text, the font face, and the color
- widget.ButtonOpts.Text("Hello, World!", face, &widget.ButtonTextColor{
+ widget.ButtonOpts.Text("000\n111\n222", face, &widget.ButtonTextColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
@@ -121,16 +118,3 @@ func loadButtonImage() (*widget.ButtonImage, error) {
Pressed: pressed,
}, nil
}
-
-func loadFont(size float64) (font.Face, error) {
- ttfFont, err := truetype.Parse(goregular.TTF)
- if err != nil {
- return nil, err
- }
-
- return truetype.NewFace(ttfFont, &truetype.Options{
- Size: size,
- DPI: 72,
- Hinting: font.HintingFull,
- }), nil
-}
Basically, replace loadFont()
with a direct use of bitmapfont.Face.
The result is off-center text. This happens with any kind of text with this font.
The font is monospaced.
It could be a bitmapfont issue or ebitenui font handling issue.
It's more likely to be a bitmapfont issue, but it's hard to debug it outside of the context.
For instance, maybe it handles glyph advance incorrectly, providing a spacing for 1 extra trailing character?
Or maybe ebitenui doesn't handle this corner case correctly with different font.Face
implementations?
The underlying font.Face
implementation can be found here: https://github.com/hajimehoshi/bitmapfont/blob/main/internal/bitmap/bitmap.go
I'll link this issue to the bitmapfont repository issue as well.
Hi, I'm not sure if this is my misunderstanding (probably!) or a layout bug.
I'm trying to put some content in the top left and bottom left of an anchor layout container (red) inside a FlipBook (blue).
But what I get is...
...or, if I swap the order of the two children of gameplayPage
(red) I get...
...but what I'm expecting is that both cyan and yellow boxes be within the red box. Nothing is expected to be in the blue area.
It feels like the first child of gameplayPage
is affecting the layout of the second child somehow?
I apologise if I've totally misunderstood how layouts or children are supposed to work, or if I've missed some config from the demo code.
The simplified repro code (based on cmd/scaffold
) is...
package main
import (
"image/color"
_ "image/png"
"io/ioutil"
"log"
"github.com/blizzy78/ebitenui"
"github.com/blizzy78/ebitenui/image"
"github.com/blizzy78/ebitenui/widget"
"github.com/golang/freetype/truetype"
"github.com/hajimehoshi/ebiten/v2"
"golang.org/x/image/font"
)
type game struct {
ui *ebitenui.UI
}
func main() {
// load button text font
face, err := loadFont("fonts/NotoSans-Regular.ttf", 20)
if err != nil {
log.Println(err)
return
}
defer face.Close()
// construct a new container that serves as the root of the UI hierarchy
rootContainer := widget.NewContainer(
// the container will use a plain color as its background
widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.RGBA{0x13, 0x1a, 0x22, 0xff})),
// the container will use an anchor layout to layout its single child widget
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
)
flipBook := widget.NewFlipBook(
widget.FlipBookOpts.Padding(widget.Insets{
Top: 50,
Left: 50,
Right: 50,
Bottom: 50,
}),
widget.FlipBookOpts.ContainerOpts(
widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.RGBA{0x00, 0x00, 0xff, 0xff})),
// get the flipbook to fill the page
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.AnchorLayoutData{
StretchHorizontal: true,
StretchVertical: true,
},
),
),
),
)
playerStatsText := widget.NewText(widget.TextOpts.Text("PLAYER STATS", face, color.Black))
playerStatsPanel := widget.NewContainer(
widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.RGBA{0xff, 0xff, 0x00, 0xff})),
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionStart,
VerticalPosition: widget.AnchorLayoutPositionStart,
},
),
),
)
nofityText := widget.NewText(widget.TextOpts.Text("NOTIFY", face, color.Black))
notifyPanel := widget.NewContainer(
widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.RGBA{0x00, 0xff, 0xff, 0xff})),
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionStart,
VerticalPosition: widget.AnchorLayoutPositionEnd,
},
),
),
)
gameplayPage := widget.NewContainer(
widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.RGBA{0xff, 0x00, 0x00, 0xff})),
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
// this is in an implicit AnchorLayout from FlipBook
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.AnchorLayoutData{
StretchHorizontal: true,
StretchVertical: true,
},
),
),
)
playerStatsPanel.AddChild(playerStatsText)
notifyPanel.AddChild(nofityText)
// NOTE: ****** swap the below two lines order to see a difference in layout, but neither as expected ******
gameplayPage.AddChild(playerStatsPanel)
gameplayPage.AddChild(notifyPanel)
flipBook.SetPage(gameplayPage)
// add the button as a child of the container
rootContainer.AddChild(flipBook)
// construct the UI
ui := ebitenui.UI{
Container: rootContainer,
}
// Ebiten setup
ebiten.SetWindowSize(400, 400)
ebiten.SetWindowTitle("Ebiten UI Scaffold")
game := game{
ui: &ui,
}
// run Ebiten main loop
if err := ebiten.RunGame(&game); err != nil {
log.Println(err)
}
}
// Update implements Game.
func (g *game) Layout(outsideWidth int, outsideHeight int) (int, int) {
return outsideWidth, outsideHeight
}
// Update implements Game.
func (g *game) Update() error {
// update the UI
g.ui.Update()
return nil
}
// Draw implements Ebiten's Draw method.
func (g *game) Draw(screen *ebiten.Image) {
// draw the UI onto the screen
g.ui.Draw(screen)
}
func loadFont(path string, size float64) (font.Face, error) {
fontData, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
ttfFont, err := truetype.Parse(fontData)
if err != nil {
return nil, err
}
return truetype.NewFace(ttfFont, &truetype.Options{
Size: size,
DPI: 72,
Hinting: font.HintingFull,
}), nil
}
In the demo application it is currently possible to delete characters (by pressing the [backspace]
or [delete]
key) typed into the Text Input widget, even when the widget is disabled.
Steps to reproduce:
[backspace]
or the [delete
] key and notice that a character is deleted from the disabled widget.Ambiguous import, cannot build example.
domain.com/project imports
github.com/hajimehoshi/ebiten/v2 imports
github.com/hajimehoshi/ebiten/v2/internal/ui imports
golang.org/x/mobile/app imports
golang.org/x/exp/shiny/driver/gldriver: ambiguous import: found package golang.org/x/exp/shiny/driver/gldriver in multiple modules:
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 (/home/me/go/pkg/mod/golang.org/x/[email protected]/shiny/driver/gldriver)
golang.org/x/exp/shiny v0.0.0-20230203172020-98cc5a0785f9 (/home/me/go/pkg/mod/golang.org/x/exp/[email protected]/driver/gldriver)
domain.com/project imports
github.com/hajimehoshi/ebiten/v2 imports
github.com/hajimehoshi/ebiten/v2/internal/ui imports
golang.org/x/mobile/app imports
golang.org/x/exp/shiny/screen: ambiguous import: found package golang.org/x/exp/shiny/screen in multiple modules:
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 (/home/me/go/pkg/mod/golang.org/x/[email protected]/shiny/screen)
golang.org/x/exp/shiny v0.0.0-20230203172020-98cc5a0785f9 (/home/me/go/pkg/mod/golang.org/x/exp/[email protected]/screen)
> mkdir project
> cd project
> go clean --modcache
> go mod init domain.com/project
> curl -O https://raw.githubusercontent.com/ebitenui/ebitenui/a1aa7778f9b2ab0189d75e96057a120a332d67b2/_examples/widget_demos/button/main.go
> go mod tidy
go: finding module for package golang.org/x/image/font/gofont/goregular
go: finding module for package golang.org/x/image/font
go: finding module for package github.com/ebitenui/ebitenui
go: finding module for package github.com/ebitenui/ebitenui/widget
go: finding module for package github.com/ebitenui/ebitenui/image
go: finding module for package github.com/golang/freetype/truetype
go: finding module for package github.com/hajimehoshi/ebiten/v2
go: downloading github.com/ebitenui/ebitenui v0.4.1
go: downloading github.com/hajimehoshi/ebiten/v2 v2.4.18
go: downloading golang.org/x/image v0.6.0
go: downloading github.com/hajimehoshi/ebiten v1.12.12
go: downloading github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
go: found github.com/ebitenui/ebitenui in github.com/ebitenui/ebitenui v0.4.1
go: found github.com/ebitenui/ebitenui/image in github.com/ebitenui/ebitenui v0.4.1
go: found github.com/ebitenui/ebitenui/widget in github.com/ebitenui/ebitenui v0.4.1
go: found github.com/golang/freetype/truetype in github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
go: found github.com/hajimehoshi/ebiten/v2 in github.com/hajimehoshi/ebiten/v2 v2.4.18
go: found golang.org/x/image/font in golang.org/x/image v0.6.0
go: found golang.org/x/image/font/gofont/goregular in golang.org/x/image v0.6.0
go: downloading github.com/stretchr/testify v1.8.1
go: downloading github.com/matryer/is v1.4.0
go: downloading github.com/hajimehoshi/file2byteslice v1.0.0
go: downloading github.com/hajimehoshi/bitmapfont/v2 v2.2.2
go: downloading golang.org/x/sys v0.5.0
go: downloading github.com/jezek/xgb v1.1.0
go: downloading golang.org/x/mobile v0.0.0-20221110043201-43a038452099
go: downloading github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading github.com/stretchr/objx v0.5.0
go: downloading golang.org/x/text v0.8.0
go: downloading golang.org/x/exp/shiny v0.0.0-20230203172020-98cc5a0785f9
go: downloading github.com/ebitengine/purego v0.1.1
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56
domain.com/project/go/pkg/mod/github.com/golang/[email protected]: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/capjoin: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/capjoin" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/drawer: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/drawer" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/freetype: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/freetype" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/gamma: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/gamma" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/genbasicfont: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/genbasicfont" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/raster: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/raster" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/round: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/round" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/truetype: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/example/truetype" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/raster: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/raster" should not have @version
domain.com/project/go/pkg/mod/github.com/golang/[email protected]/truetype: import path "domain.com/project/go/pkg/mod/github.com/golang/[email protected]/truetype" should not have @version
domain.com/project imports
github.com/hajimehoshi/ebiten/v2 imports
github.com/hajimehoshi/ebiten/v2/internal/ui imports
golang.org/x/mobile/app imports
golang.org/x/exp/shiny/driver/gldriver: ambiguous import: found package golang.org/x/exp/shiny/driver/gldriver in multiple modules:
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 (/home/me/go/pkg/mod/golang.org/x/[email protected]/shiny/driver/gldriver)
golang.org/x/exp/shiny v0.0.0-20230203172020-98cc5a0785f9 (/home/me/go/pkg/mod/golang.org/x/exp/[email protected]/driver/gldriver)
domain.com/project imports
github.com/hajimehoshi/ebiten/v2 imports
github.com/hajimehoshi/ebiten/v2/internal/ui imports
golang.org/x/mobile/app imports
golang.org/x/exp/shiny/screen: ambiguous import: found package golang.org/x/exp/shiny/screen in multiple modules:
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 (/home/me/go/pkg/mod/golang.org/x/[email protected]/shiny/screen)
golang.org/x/exp/shiny v0.0.0-20230203172020-98cc5a0785f9 (/home/me/go/pkg/mod/golang.org/x/exp/[email protected]/screen)
> go build
main.go:7:2: no required module provides package github.com/ebitenui/ebitenui; to add it:
go get github.com/ebitenui/ebitenui
main.go:8:2: no required module provides package github.com/ebitenui/ebitenui/image; to add it:
go get github.com/ebitenui/ebitenui/image
main.go:9:2: no required module provides package github.com/ebitenui/ebitenui/widget; to add it:
go get github.com/ebitenui/ebitenui/widget
main.go:10:2: no required module provides package github.com/golang/freetype/truetype; to add it:
go get github.com/golang/freetype/truetype
main.go:11:2: no required module provides package github.com/hajimehoshi/ebiten/v2; to add it:
go get github.com/hajimehoshi/ebiten/v2
main.go:12:2: no required module provides package golang.org/x/image/font; to add it:
go get golang.org/x/image/font
main.go:13:2: no required module provides package golang.org/x/image/font/gofont/goregular; to add it:
go get golang.org/x/image/font/gofont/goregular
Starting with ebitenui v0.5.0 when the ability to scroll to the selected entry in a combo box was added, there is an issue where if the list is long enough to have more than two "pages", the initial selection of any entry in the middle of these pages causes the scroll list to toggle back and forth between the very top and very bottom page, never able to get to the middle.
The problem appears to be somewhere in here:
v0.4.4...v0.5.0#diff-c250f77002aadfcf2a0bac1450056d4563eed035dc3e7948cf06ded8149bf549R449
It looks like those functions setScrollTop/setScrollLeft were meant to be fractions of much less than 1, but the recent changes has it pass in the number of pixels between the current and target location.
Hey I'm having an issue with text inputs where they get two characters printed per key stroke, this is also reproducible on the _examples/demo did some debugging and modifying the textinput.go
func (t *TextInput) doInsert(c []rune) {
s := string(insertChars([]rune(t.InputText), c, t.cursorPosition))
time.Sleep(50 * time.Millisecond)
if t.validationFunc != nil {
result, replacement := t.validationFunc(s)
if !result {
if replacement != nil {
s = *replacement
} else {
return
}
}
}
t.InputText = s
t.cursorPosition += len(c)
}
Seems to fix this, although this does not feel like the real fix.
Just managed to build my game for Android (hooray!), but there doesn't appear to be any touch input support in ebintenui?
I'm happy to take on this issue, internal/input/input.go
looks like the right place to start?
Hi, I working in a demo "login" view for my game.
I struggling to understand how I can center the fields into center of a container, this is possible now? Or I need to use a sized panel to center the inside containers and fix the width and height to control the position of the widgets inside a container?
The code:
// Create the container
rootContainer := widget.NewContainer(
widget.ContainerOpts.Layout(
widget.NewGridLayout(
widget.GridLayoutOpts.Columns(1),
widget.GridLayoutOpts.Stretch([]bool{true}, []bool{false, true, false}),
widget.GridLayoutOpts.Padding(widget.Insets{
Top: 20,
Bottom: 20,
Left: 20,
Right: 20,
}),
widget.GridLayoutOpts.Spacing(0, 20)),
),
widget.ContainerOpts.BackgroundImage(res.Panel.Image),
)
b := widget.NewButton(
widget.ButtonOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})),
widget.ButtonOpts.Image(res.Button.Image),
widget.ButtonOpts.Text(fmt.Sprintf("Log In"), res.Button.Face, res.Button.Text),
widget.ButtonOpts.TextPadding(res.Button.Padding),
widget.ButtonOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
}),
),
)
rootContainer.AddChild(b)
`func (t *TextInput) idleState(newKeyOrCommand bool) textInputState {
.......
chars := input.InputChars()
if len(chars) > 0 {
return t.charInputState(chars[0]), true //多个字一起传入 return t.charInputState(chars), tru
}
......
}
func (t *TextInput) doInsert(c []rune) {
.....
t.cursorPosition += len(c) //一次加多个字符位
}
func fontStringIndex(s string, f font.Face, x int) int {
start := 0
end := len([]rune(s)) //convert to []rune
line(528) sub := string([]rune(s)[:p]) 不是有可能会出现out of index
`
Running a demo example in browser leads to "not implemented on js" when trying to read assets.
Perhaps they should be embedded via "go:embed"?
I may try making it work in wasm and figure out the real reason why it doesn't work there.
ebitenui/_examples/demo/main.go
Lines 63 to 72 in 24d44f4
It's not very clear for the unexperienced user why a single-column grid layout is used instead of a row layout.
If there is a reason for that, it should probably be stated in the comment.
From a user point of view, a single column grid shouldn't be very different from the row layout.
Using ebitenui v0.4.0, I'm creating some labeled checkboxes and sometimes they need to be checked on by default. However, the .Checkbox() function returns nil after initial creation, which is needed to access its .SetState() function. It appears the checkbox object underneath doesn't get set until after the .init.Do() function is called, which is only performed after first calling one of a few functions which aren't always necessary to do up front (GetWidget, PreferredSize, SetLocation, SetupInputLayer, Render, Focus).
At the least I think it would be worth adding a SetState() function directly on the labeled checkbox object which can call the .init.Do() first to ensure it will be present.
do you know is this is eventually planned to be upstreamed to ebiten and so supported there ?
It would be great if there was UI scale factor, to enlarge/shrink the entire UI.
People can implement it themselves, but it would be nice if the library just took care of it.
GitHub pages on https://ebitenui.github.io/ could have an online demo.
With #50 it should be possible to build _examples/demo
for wasm.
cd _examples/demo
GOOS=js GOARCH=wasm go build -o main.wasm .
An example of this is https://quasilyte.dev/ebitenui/
(https://github.com/quasilyte/quasilyte.github.io/tree/master/ebitenui)
It could be embedded in the page in such a way that it looks more appropriate for the docs site.
Currently is doe not seem possible to change the entries in a List after creation.
i.E. a list like this:
entries := []interface{}{
"Test 1",
"Test 2",
"Test 3",
}
list := widget.NewList(
widget.ListOpts.Entries(entries),
widget.ListOpts.EntryLabelFunc(func(e interface{}) string {
return e.(string)
}),
[...]
[...]
)
When the slice is changed end set again for the given list only the underlying model is changed.
The widgets (buttons/labels) are not updated/removed/added.
For example:
entries[0] = "Changed 1"
widget.ListOpts.Entries(entries)(list)
Result:
Visual change: none
Bahaviour change: The changed list entry can no longer be selected
entries = append(entries, "Teste New")
widget.ListOpts.Entries(entries)(list)
Result:
Visual change: none
Bahaviour change: none
entries = []interface{}{}
widget.ListOpts.Entries(entries)(list)
Result:
Visual change: none
Bahaviour change: Clicking an entry crashes the program because of index out of bounds.
Am I missing something/doing something wrong?
Or is it not possible to change the model of a list after creation?
Currently the only way I found to update a List is by recreating the entire list, which is quite costly.
I also played around with widget.ScrollContainerOpts.Content
to set my own container with contents that I could update. But I had no luck to get this to work at all.
Thanks for the great library!
Hi,
I'm just trying out ebitenui and had a question. I have different UIs on different screens and I'm not sure if each screen should be their own separate ebitenui.UI
s or if they should all be structured to be within a single ebitenui.UI
?
Thank you <3
When a labeled checkbox is added to a moveable window, it does not move with it, instead staying in the same place it was where the window was first opened.
Example code: v0.4.1...harbdog:ebitenui:window_checkbox_issue
Hi, I have the following "login" window:
func setLoginContainerWindow(res *assets.UiResources, ui func() *ebitenui.UI) {
var rw ebitenui.RemoveWindowFunc
c := widget.NewContainer(
widget.ContainerOpts.BackgroundImage(res.Panel.Image),
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(res.Panel.Padding),
widget.RowLayoutOpts.Spacing(15),
)),
)
c.AddChild(widget.NewText(
widget.TextOpts.Text("Login", res.Text.BigTitleFace, res.Text.IdleColor),
))
tOpts := []widget.TextInputOpt{
widget.TextInputOpts.WidgetOpts(
// instruct the container's anchor layout to center the button
// both horizontally and vertically
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
}),
),
widget.TextInputOpts.Image(res.TextInput.Image),
widget.TextInputOpts.Color(res.TextInput.Color),
widget.TextInputOpts.Padding(widget.Insets{
Left: 13,
Right: 13,
Top: 7,
Bottom: 7,
}),
widget.TextInputOpts.Face(res.TextInput.Face),
widget.TextInputOpts.CaretOpts(
widget.CaretOpts.Size(res.TextInput.Face, 2),
),
}
usernameInput := widget.NewTextInput(append(
tOpts,
widget.TextInputOpts.Placeholder("Username"))...,
)
c.AddChild(usernameInput)
passwordInput := widget.NewTextInput(append(
tOpts,
widget.TextInputOpts.Secure(true),
widget.TextInputOpts.Placeholder("Password"))...,
)
c.AddChild(passwordInput)
c1 := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionHorizontal),
widget.RowLayoutOpts.Spacing(10),
)),
)
c.AddChild(c1)
c1.AddChild(widget.NewButton(
widget.ButtonOpts.Image(res.Button.Image),
widget.ButtonOpts.TextPadding(res.Button.Padding),
widget.ButtonOpts.Text("Log in", res.Button.Face, res.Button.Text),
widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) {
// Do login here
}),
))
c1.AddChild(widget.NewButton(
widget.ButtonOpts.Image(res.Button.Image),
widget.ButtonOpts.TextPadding(res.Button.Padding),
widget.ButtonOpts.Text("Cancel", res.Button.Face, res.Button.Text),
widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) {
rw()
}),
))
w := widget.NewWindow(widget.WindowOpts.Modal(), widget.WindowOpts.Contents(c))
ww, wh := ebiten.WindowSize()
r := image.Rect(0, 0, ww*3/4, wh/3)
r = r.Add(image.Point{X: ww / 4 / 2, Y: wh * 2 / 3 / 2})
w.SetLocation(r)
rw = ui().AddWindow(w)
//usernameInput.Focus(true)
}
I notice if I try to click into one of the inputs they not change the status to focused. I have done some wrong or is a common behavior to set focus to true when open an window?
Hello I've stumbled upon a problem with the rendered button image, if the input image was cropped before creating the nineslice, then it stops being displayed, while everything is fine with the image itself.
This is spritesheet for button image:
This is my crop function:
func CropSingle(source *ebiten.Image, x, y, w, h int) *ebiten.Image {
return source.SubImage(Rect(x, y, w, h)).(*ebiten.Image)
}
This is loading the atlas and the image for the buttons:
w, h, corner, center := 64, 64, 12, 40
sheet := load.Wrap(load.Image(static, "images/ui.png"))
imageA := CropSingle(sheet, 0, 0, w, h)
imageB := CropSingle(sheet, w, 0, w, h)
buttonImageA := &widget.ButtonImage{
Idle: image.NewNineSliceSimple(imageA, corner, center),
}
buttonImageB := &widget.ButtonImage{
Idle: image.NewNineSliceSimple(imageB, corner, center),
}
At the moment I am drawing the original image in the corner and a set of buttons based on it
func Update() error {
container.Update()
return nil
}
func Draw(screen *ebiten.Image) {
screen.Fill(colornames.Black)
container.Draw(screen)
screen.DrawImage(<imageA / imageB>, nil)
}
When I use imageA as the basis for the buttons, everything is ok:
But when I use imageB, the buttons stop rendering, although they are done exactly the same, and the image is cut correctly:
Since most of the time it looks like we need widget.PreferredSizeLocateableWidget
for AddChild()
argument, perhaps there could be a shorthand for widget.PreferredSizeLocateableWidget
? It can't be widget.Widget
, since that name is already used, but the current way of spelling "an arbitrary widget that can be a child of another widget" is quite verbose and not very intuitive.
This is just a suggestion though. The library users can define an alias on their own like:
type Widget = widget.PreferredSizeLocateableWidget
Why do we even need to spell that type in the first place? Well, mostly when you want to define some sort of a factory function that may return an arbitrary UI widget. Any forms of abstractions on top of widgets would like to avoid spelling the concrete widget type directly in my opinion. If we want to have a collection of widgets (a slice), it should also be more concise to say []Widget
(our local type alias) instead of []ebitenui.PreferredSizeLocateableWidget
but having a library-bundled alias could make writing tutorials a little bit easier.
Note that widget.PreferredSizeLocateableWidget
is used inside the examples/demo
too. So it's not a totally artificial use case. :)
There is var clockTime string
.
This updates the variable.
func (g *game) Update() error {
clockTime = time.Now().Format("3:04 pm")
However, the updated clockTime never shows.
It only shows the initialized value from when it was added.
c.AddChild(makeHeaderLine(res.header.background, res.header.padding, res.header.color, res.header.face, clockTime, []widget.ContainerOpt{}))
I've tried setting this to true
.
ebiten.SetScreenClearedEveryFrame(true)
And I have also tried to request a relayout that I found in the docs..
For reference, the makeHeaderLine function is creating the clock face as a widget.NewText.
func makeHeaderLine(background *image.NineSlice, padding widget.Insets, color color.Color, face font.Face, message string, containerOpts []widget.ContainerOpt) *widget.Container {
// Container options
containerOpts = append(containerOpts, widget.ContainerOpts.BackgroundImage(background))
containerOpts = append(containerOpts, widget.ContainerOpts.Layout(widget.NewAnchorLayout(widget.AnchorLayoutOpts.Padding(padding))))
containerOpts = append(containerOpts, widget.ContainerOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})))
// Create the container
headerContainer := widget.NewContainer(containerOpts...)
// Text options
textOpts := []widget.TextOpt{
widget.TextOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
})),
widget.TextOpts.Text(message, face, color),
widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter),
}
// Create the text and add it to the container
headerText := widget.NewText(textOpts...)
headerContainer.AddChild(headerText)
return headerContainer
}
How do I show the updated text?
Update:
I was digging around and I saw that Ebitengine has BoundString for text in the example, which works (if you change the sampleText to var and put a counter in the Update func). However, I don't see that exposed in the text for EbitenUI.
This leads me to believe that the solution in EbitenUI is implemented somewhere completely different. If not, could this be a feature request?
If that's possible, it would be good to add this as a separate example (it could be an addition to "demo" example, but it's so big already, very overwhelming).
Most real-life UIs should have a way to handle input focus switching. Otherwise, we're making the player rely solely on the mouse (which is inconvenient if the game can be played without the mouse at all).
This is mostly useful in menus with simple layout like this:
[ button1 ] // <- focused by default
[ button2 ]
[ button3 ]
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.