Code Monkey home page Code Monkey logo

ebitenui's Introduction

GitHub release (latest by date including pre-releases) GitHub GoDoc GoDoc

Discord Subreddit subscribers

Ebiten UI

A user interface engine and widget library for Ebitengine

>> Note: This library is separate from Ebitengine. Please reach out to the linked Discord server for questions or suggestions

Ebiten UI is an extension to Ebitengine that provides the ability to render a complete user interface, with widgets such as buttons, lists, combo boxes, and so on. It uses the [retained mode] model. All graphics used by Ebiten UI can be fully customized, so you can really make your UI your own.

Documentation on how to use and extend Ebiten UI is available at ebitenui.github.io.

Screenshots

Quick Start

Ebiten UI is written in Go 1.19 which is available at https://go.dev/.

There are Ebiten UI examples that can be found in the _examples/ folder.

They can be run from the root directory of the project with the following commands:

  • Ebiten UI complete demo: go run github.com/ebitenui/ebitenui/_examples/demo
  • Ebiten UI widget: go run github.com/ebitenui/ebitenui/_examples/widget_demos/<folder_name>

The examples can also be tested as WASM by running the following commands and opening your browser to http://localhost:6262:

  • Ebiten UI complete demo: go run github.com/hajimehoshi/wasmserve@latest -http=:6262 ./_examples/demo
  • Ebiten UI widget: go run github.com/hajimehoshi/wasmserve@latest -http=:6262 ./_examples/widget_demos/<folder_name>

Used By

Social Media

License

Ebiten UI is licensed under the MIT license.

Maintainers

Contributing

Want to help develop Ebiten UI? Check out our current issues. Want to know the steps on how to start contributing, take a look at the open source guide.

ebitenui's People

Contributors

blizzy78 avatar corfe83 avatar ethanmdavidson avatar gucio321 avatar harbdog avatar hovatterz avatar kevinvlaanderen avatar kidk avatar mat007 avatar mcarpenter622 avatar nmorenor avatar ntsd avatar quasilyte avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ebitenui's Issues

_demo: possible to delete characters even when Text Input widget is disabled

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:

  1. Navigate to the Text Input widget in the demo application.
  2. Enter text (e.g. "abcd").
  3. Disable the widget.
  4. Now, use the mouse to position the text carret in the Text Input widget.
  5. Press the [backspace] or the [delete] key and notice that a character is deleted from the disabled widget.

screenshot_2020-12-31_00:25:04

SVG Vector Option in Widgets

Summary

Requesting solution or support for vector-based option to replace the remaining PNGs. (Combobox and Checkbox)

Situation

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.)

Load Graphic Images Function

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
}

Altered Resources.go File

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,
	}
}

Issue pre-selecting entry in middle of combo box with multiple pages of entries

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.

combo_list_issue

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.

Editable Text Area.

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.

CursorUpdater: how to set back to default?

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).

_demo: dragging outside of application window results in crash.

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).

screenshot_2020-12-31_00:19:38

$ _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

How to update text when var changes

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?

Touch support

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?

Consider adding a comment for the root container of demo example

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,
}),
widget.GridLayoutOpts.Spacing(0, 20))),
widget.ContainerOpts.BackgroundImage(res.background))

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.

Text centering issues with bitmapfont

  1. Modify the button widget example as follows:
--- 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.

  1. Run the example.

image

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.

Canvas/generic-image widget?

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?

Maybe add some alias to PreferredSizeLocateableWidget?

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. :)

Tooltip display when it doesn't fit the screen

image

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.

Creating nine-slice from cropped images don't work as expected

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:
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:

image

But when I use imageB, the buttons stop rendering, although they are done exactly the same, and the image is cut correctly:

image

Add arrows switch widget focus to the example

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 ]

UI Scale factor

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.

Changing of List entries not possible after creation?

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!

ไธ€ๆฌกๅฝ•ๅ…ฅๅคšไธชๅญ— Input multiple words at one time

`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
`

Improve the grid-like container navigation

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:

  • horizontal layout: prev=left, next=right, up and down=nothing
  • vertical layout: prev=up, next=down, left and right=nothing

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.

Exception trying to set labeled checkbox state just after creation

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.

A disabled text input can still be focused, the cursor is blinking

image

I think this behavior can confuse users if disabled input images are not obvious enough.
The user would think that there is a problem with their keyboard (or other input method).

I would assume that disabled element can't receive focus; or at least the text cursor shouldn't show up (neither blink).

Make "demo" example working in browser (wasm build)

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.

CursorUpdater: a cursor disappears with nil replacement images

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:

  1. There can be two interfaces. The current (full) interface and headless (new) interface for logic-only.
  2. The current interface may handle nil images as "use the defaults" sign

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.

How to center middle a Stretch container

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)

No "hover" image in TextInputImage

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.

[QUESTION] Textinput cannot be "clickable" inside Window?

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?

TextInput rendering two characters per keystroke

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.

How to hide a widget?

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).

Ambiguous import, cannot build example

Ambiguous import, cannot build example.

Error

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)

Full example

> 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

Multiple children in anchored container bug/misunderstanding?

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...

Screenshot 2020-10-17 at 21 34 21

...or, if I swap the order of the two children of gameplayPage (red) I get...

Screenshot 2020-10-17 at 21 36 50

...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
}

Text input cursor is not at the end when text is updated through the code

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.

Rendering on an image that is not the entire screen get button events on the wrong place

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.