From 84d8216f2b104b05c15217d5245b8a3bb42db201 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 <50602525+Vmarcelo49@users.noreply.github.com> Date: Sat, 28 Jun 2025 23:58:07 -0300 Subject: [PATCH 01/12] WIP Dropdown menu --- dropdown.go | 210 +++++++++++++++++++++++++++++++++++++++ example/dropdown/main.go | 128 ++++++++++++++++++++++++ widget.go | 1 + 3 files changed, 339 insertions(+) create mode 100644 dropdown.go create mode 100644 example/dropdown/main.go diff --git a/dropdown.go b/dropdown.go new file mode 100644 index 0000000..d0ab8aa --- /dev/null +++ b/dropdown.go @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: MIT +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +package debugui + +import ( + "image" +) + +// DropdownID is the ID of a dropdown menu container. +type DropdownID widgetID + +// Dropdown creates a dropdown menu widget that allows users to select from a list of options. +// selectedIndex is a pointer to the currently selected option index (0-based). +// options is a slice of strings representing the available choices. +// Returns an EventHandler that triggers when the selection changes. +func (c *Context) Dropdown(selectedIndex *int, options []string) EventHandler { + pc := caller() + id := c.idFromCaller(pc) + return c.wrapEventHandlerAndError(func() (EventHandler, error) { + return c.dropdown(selectedIndex, options, id) + }) +} + +func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (EventHandler, error) { + if selectedIndex == nil { + // Return null handler if selectedIndex pointer is nil to prevent panics + return &nullEventHandler{}, nil + } + + if len(options) == 0 { + // Return null handler for empty options to prevent rendering issues + return &nullEventHandler{}, nil + } + + // Clamp selectedIndex to valid range to prevent out-of-bounds access + if *selectedIndex < 0 || *selectedIndex >= len(options) { + *selectedIndex = 0 + } + last := *selectedIndex + + dropdownID := DropdownID(c.idFromString("dropdown:" + string(id))) + + // Ensure dropdown container always exists (create it if needed) + dropdownContainer := c.container(widgetID(dropdownID), 0) + + // Start with the dropdown closed + if dropdownContainer.layout.Bounds.Empty() { + dropdownContainer.open = false + } + + _ = c.wrapEventHandlerAndError(func() (EventHandler, error) { + windowOptions := optionDropdown | optionNoResize | optionNoTitle | optionClosed // optionClosed doest seem to work like i expected, but we handle closing manually + + if err := c.window("", image.Rectangle{}, windowOptions, widgetID(dropdownID), func(layout ContainerLayout) { + // Ensure dropdown container reference is fresh for each render + if cnt, exists := c.getDropdownContainer(dropdownID); exists { + if cnt.open { + c.bringToFront(cnt) + } + } + + // full width dropdown + c.SetGridLayout([]int{-1}, nil) + + // Render each dropdown option as a clickable button + for i, option := range options { + c.IDScope(option, func() { + isSelected := i == *selectedIndex + + // Highlight the currently selected option + var buttonColor int + if isSelected { + buttonColor = colorButtonFocus + } else { + buttonColor = colorButton + } + + // Create clickable button widget for this option + pc := caller() + buttonID := c.idFromCaller(pc) + var wasPressed bool + + _ = c.wrapEventHandlerAndError(func() (EventHandler, error) { + e, err := c.widget(buttonID, optionAlignCenter, nil, func(bounds image.Rectangle, wasFocused bool) EventHandler { + var e EventHandler + + if c.pointing.justPressed() && c.focus == buttonID { + // Handle option selection + wasPressed = true + e = &eventHandler{} + } + + return e + }, func(bounds image.Rectangle) { + // Draw the option button with appropriate styling + c.drawWidgetFrame(buttonID, bounds, buttonColor, optionAlignCenter) + if len(option) > 0 { + c.drawWidgetText(option, bounds, colorText, optionAlignCenter) + } + }) + return e, err + }) + + // Handle option selection: update index and close dropdown + if wasPressed { + *selectedIndex = i + if cnt, exists := c.getDropdownContainer(dropdownID); exists { + cnt.open = false + } + } + }) + } + }); err != nil { + return nil, err + } + return nil, nil + }) + + // Create the main dropdown button that toggles the menu + return c.widget(id, optionAlignCenter, nil, func(bounds image.Rectangle, wasFocused bool) EventHandler { + var e EventHandler + + // Ensure dropdown is always brought to front when hovering over it while open + dropdownContainer, exists := c.getDropdownContainer(dropdownID) + if exists && dropdownContainer.open { + clickPos := c.pointingPosition() + if clickPos.In(dropdownContainer.layout.Bounds) { + c.bringToFront(dropdownContainer) + } + } + + // Manual "click outside to close" and dropdown toggle, trying to do this in the container.go had lots of issues + if exists && dropdownContainer.open && c.pointing.justPressed() { + clickPos := c.pointingPosition() + clickInButton := clickPos.In(bounds) + clickInDropdown := clickPos.In(dropdownContainer.layout.Bounds) + + if !clickInButton && !clickInDropdown { + dropdownContainer.open = false + } + } + + // Toggle dropdown when button is clicked + if c.pointing.justPressed() && c.focus == id { + // Check if dropdown container exists and its state + dropdownContainer, _ := c.getDropdownContainer(dropdownID) + + isOpen := dropdownContainer.open + + if isOpen { + // Close the dropdown + dropdownContainer.open = false + } else { + // Store the current state before opening, made in some desperate attempts to avoid feedback loops + wasClosedBefore := !dropdownContainer.open + + // Open the dropdown + dropdownContainer.open = true + + // Position dropdown directly below button with proper width + if wasClosedBefore { + dropdownPos := image.Pt(bounds.Min.X, bounds.Max.Y) + buttonWidth := bounds.Dx() + optionHeight := c.style().defaultHeight + c.style().padding + estimatedHeight := len(options) * optionHeight + dropdownContainer.layout.Bounds = image.Rectangle{ + Min: dropdownPos, + Max: dropdownPos.Add(image.Pt(buttonWidth, estimatedHeight)), + } + } + + c.bringToFront(dropdownContainer) + } + } + + // Generate event if user changed selection + if last != *selectedIndex { + e = &eventHandler{} + } + + return e + }, func(bounds image.Rectangle) { + // Draw the dropdown button appearance + c.drawWidgetFrame(id, bounds, colorButton, optionAlignCenter) + + // Show currently selected text (reserve space for arrow - use widget height for square arrow area) + arrowWidth := bounds.Dy() + textBounds := bounds + textBounds.Max.X -= arrowWidth + c.drawWidgetText(options[*selectedIndex], textBounds, colorText, optionAlignCenter) + + // Draw dropdown arrow indicator (up/down based on current state) + dropdownContainer, exists := c.getDropdownContainer(dropdownID) + isOpen := exists && dropdownContainer.open + + arrowBounds := image.Rect(bounds.Max.X-arrowWidth, bounds.Min.Y, bounds.Max.X, bounds.Max.Y) + icon := iconDown + if isOpen { + icon = iconUp + } + c.drawIcon(icon, arrowBounds, c.style().colors[colorText]) + }) +} + +// getDropdownContainer retrieves the dropdown container by its ID and a bool to indicate if it exists. +func (c *Context) getDropdownContainer(dropdownID DropdownID) (*container, bool) { + container, exists := c.idToContainer[widgetID(dropdownID)] + return container, exists +} diff --git a/example/dropdown/main.go b/example/dropdown/main.go new file mode 100644 index 0000000..dbf0191 --- /dev/null +++ b/example/dropdown/main.go @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 The Ebitengine Authors + +package main + +import ( + "fmt" + "image" + "log" + "os" + + "github.com/ebitengine/debugui" + "github.com/hajimehoshi/ebiten/v2" +) + +type Game struct { + debugUI *debugui.DebugUI + + // Dropdown demo data + resolutionOptions []string + selectedResolution int + + qualityOptions []string + selectedQuality int + + nameOptions []string + selectedName int + + // Log for events + logBuffer []string +} + +func NewGame() *Game { + return &Game{ + debugUI: &debugui.DebugUI{}, + + resolutionOptions: []string{"720p", "1080p", "1440p", "4K", "Can it run Crysis?"}, + selectedResolution: 1, // Default to 1080p + + qualityOptions: []string{"Low", "Medium", "High", "Ultra"}, + selectedQuality: 2, // Default to High + + nameOptions: []string{"Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Gopher", "Hajime", "Isabella", "Jack"}, + selectedName: 6, // Default to Gopher + + logBuffer: []string{}, + } +} + +func (g *Game) addLog(message string) { + g.logBuffer = append(g.logBuffer, message) + // Keep only last 10 messages + if len(g.logBuffer) > 10 { + g.logBuffer = g.logBuffer[1:] + } +} + +func (g *Game) Update() error { + _, err := g.debugUI.Update(func(ctx *debugui.Context) error { + // Main demo window + ctx.Window("Simple Dropdown Demo", image.Rect(50, 50, 400, 280), func(layout debugui.ContainerLayout) { + ctx.Header("Settings", true, func() { + ctx.SetGridLayout([]int{100, -1}, nil) + + // Resolution dropdown + ctx.Text("Resolution:") + ctx.Dropdown(&g.selectedResolution, g.resolutionOptions).On(func() { + g.addLog(fmt.Sprintf("Resolution: %s", g.resolutionOptions[g.selectedResolution])) + }) + + // Quality dropdown + ctx.Text("Quality:") + ctx.Dropdown(&g.selectedQuality, g.qualityOptions).On(func() { + g.addLog(fmt.Sprintf("Quality: %s", g.qualityOptions[g.selectedQuality])) + }) + + // Name dropdown + ctx.Text("Name:") + ctx.Dropdown(&g.selectedName, g.nameOptions).On(func() { + g.addLog(fmt.Sprintf("Name: %s", g.nameOptions[g.selectedName])) + }) + }) + + ctx.Header("Current Selection", false, func() { + ctx.Text(fmt.Sprintf("Resolution: %s", g.resolutionOptions[g.selectedResolution])) + ctx.Text(fmt.Sprintf("Quality: %s", g.qualityOptions[g.selectedQuality])) + ctx.Text(fmt.Sprintf("Name: %s", g.nameOptions[g.selectedName])) + }) + + ctx.Header("Actions", false, func() { + ctx.SetGridLayout([]int{-1, -1}, nil) + ctx.Button("Reset Settings").On(func() { + g.selectedResolution = 1 + g.selectedQuality = 2 + g.selectedName = 6 + g.addLog("Settings reset to defaults") + }) + ctx.Button("Clear Log").On(func() { + g.logBuffer = []string{} + }) + }) + }) + + return nil + }) + + return err +} + +func (g *Game) Draw(screen *ebiten.Image) { + g.debugUI.Draw(screen) +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return outsideWidth, outsideHeight +} + +func main() { + ebiten.SetWindowTitle("Simple Dropdown Demo") + ebiten.SetWindowSize(800, 600) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + + game := NewGame() + if err := ebiten.RunGame(game); err != nil { + log.Fatal(err) + os.Exit(1) + } +} diff --git a/widget.go b/widget.go index 1d03c6a..5e9a770 100644 --- a/widget.go +++ b/widget.go @@ -28,6 +28,7 @@ const ( optionHoldFocus optionAutoSize optionPopup + optionDropdown optionClosed optionExpanded ) From f62d39542a03d5a9ad2660ca4be5c1dde66cf58e Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Tue, 1 Jul 2025 11:33:01 -0300 Subject: [PATCH 02/12] Removing some code that doesnt do anything and improving the example to show the current issue in a better way --- dropdown.go | 46 ++++++++++------------------------------ example/dropdown/main.go | 5 ++++- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/dropdown.go b/dropdown.go index d0ab8aa..1e21126 100644 --- a/dropdown.go +++ b/dropdown.go @@ -23,16 +23,10 @@ func (c *Context) Dropdown(selectedIndex *int, options []string) EventHandler { } func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (EventHandler, error) { - if selectedIndex == nil { - // Return null handler if selectedIndex pointer is nil to prevent panics + if selectedIndex == nil || len(options) == 0 { + // If no options or selectedIndex is nil, return a null event handler return &nullEventHandler{}, nil } - - if len(options) == 0 { - // Return null handler for empty options to prevent rendering issues - return &nullEventHandler{}, nil - } - // Clamp selectedIndex to valid range to prevent out-of-bounds access if *selectedIndex < 0 || *selectedIndex >= len(options) { *selectedIndex = 0 @@ -42,19 +36,21 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E dropdownID := DropdownID(c.idFromString("dropdown:" + string(id))) // Ensure dropdown container always exists (create it if needed) + dropdownContainer := c.container(widgetID(dropdownID), 0) // Start with the dropdown closed + if dropdownContainer.layout.Bounds.Empty() { dropdownContainer.open = false } _ = c.wrapEventHandlerAndError(func() (EventHandler, error) { - windowOptions := optionDropdown | optionNoResize | optionNoTitle | optionClosed // optionClosed doest seem to work like i expected, but we handle closing manually + windowOptions := optionDropdown | optionNoResize | optionNoTitle if err := c.window("", image.Rectangle{}, windowOptions, widgetID(dropdownID), func(layout ContainerLayout) { // Ensure dropdown container reference is fresh for each render - if cnt, exists := c.getDropdownContainer(dropdownID); exists { + if cnt := c.container(widgetID(dropdownID), 0); cnt != nil { if cnt.open { c.bringToFront(cnt) } @@ -105,8 +101,8 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E // Handle option selection: update index and close dropdown if wasPressed { *selectedIndex = i - if cnt, exists := c.getDropdownContainer(dropdownID); exists { - cnt.open = false + if cnt := c.container(widgetID(dropdownID), 0); cnt != nil { + cnt.open = false // Close the dropdown when an option is selected } } }) @@ -121,17 +117,9 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E return c.widget(id, optionAlignCenter, nil, func(bounds image.Rectangle, wasFocused bool) EventHandler { var e EventHandler - // Ensure dropdown is always brought to front when hovering over it while open - dropdownContainer, exists := c.getDropdownContainer(dropdownID) - if exists && dropdownContainer.open { - clickPos := c.pointingPosition() - if clickPos.In(dropdownContainer.layout.Bounds) { - c.bringToFront(dropdownContainer) - } - } - + dropdownContainer := c.container(widgetID(dropdownID), 0) // Manual "click outside to close" and dropdown toggle, trying to do this in the container.go had lots of issues - if exists && dropdownContainer.open && c.pointing.justPressed() { + if dropdownContainer.open && c.pointing.justPressed() { clickPos := c.pointingPosition() clickInButton := clickPos.In(bounds) clickInDropdown := clickPos.In(dropdownContainer.layout.Bounds) @@ -144,7 +132,6 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E // Toggle dropdown when button is clicked if c.pointing.justPressed() && c.focus == id { // Check if dropdown container exists and its state - dropdownContainer, _ := c.getDropdownContainer(dropdownID) isOpen := dropdownContainer.open @@ -169,8 +156,6 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E Max: dropdownPos.Add(image.Pt(buttonWidth, estimatedHeight)), } } - - c.bringToFront(dropdownContainer) } } @@ -191,20 +176,11 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E c.drawWidgetText(options[*selectedIndex], textBounds, colorText, optionAlignCenter) // Draw dropdown arrow indicator (up/down based on current state) - dropdownContainer, exists := c.getDropdownContainer(dropdownID) - isOpen := exists && dropdownContainer.open - arrowBounds := image.Rect(bounds.Max.X-arrowWidth, bounds.Min.Y, bounds.Max.X, bounds.Max.Y) icon := iconDown - if isOpen { + if c.container(widgetID(dropdownID), 0).open { icon = iconUp } c.drawIcon(icon, arrowBounds, c.style().colors[colorText]) }) } - -// getDropdownContainer retrieves the dropdown container by its ID and a bool to indicate if it exists. -func (c *Context) getDropdownContainer(dropdownID DropdownID) (*container, bool) { - container, exists := c.idToContainer[widgetID(dropdownID)] - return container, exists -} diff --git a/example/dropdown/main.go b/example/dropdown/main.go index dbf0191..f1d5072 100644 --- a/example/dropdown/main.go +++ b/example/dropdown/main.go @@ -73,7 +73,10 @@ func (g *Game) Update() error { ctx.Dropdown(&g.selectedQuality, g.qualityOptions).On(func() { g.addLog(fmt.Sprintf("Quality: %s", g.qualityOptions[g.selectedQuality])) }) - + ctx.Text("Reset Quality:") + ctx.Button("Reset").On(func() { // used to debug the dropdown above, clicking low also clicks on this + g.selectedQuality = 2 // Reset to High + }) // Name dropdown ctx.Text("Name:") ctx.Dropdown(&g.selectedName, g.nameOptions).On(func() { From 148503cec0f9a105c807f603661fdec90949d880 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Tue, 1 Jul 2025 18:06:38 -0300 Subject: [PATCH 03/12] Fixing the widgets below getting clicks after dropdowns close --- container.go | 3 +++ dropdown.go | 28 +++++++++++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/container.go b/container.go index 6b61f26..6536ade 100644 --- a/container.go +++ b/container.go @@ -24,6 +24,9 @@ type container struct { toggledIDs map[widgetID]struct{} textInputTextFields map[widgetID]*textinput.Field + // closeDelay is used for delayed closing of dropdowns + closeDelay int + used bool } diff --git a/dropdown.go b/dropdown.go index 1e21126..b774803 100644 --- a/dropdown.go +++ b/dropdown.go @@ -5,6 +5,8 @@ package debugui import ( "image" + + "github.com/hajimehoshi/ebiten/v2" ) // DropdownID is the ID of a dropdown menu container. @@ -36,11 +38,17 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E dropdownID := DropdownID(c.idFromString("dropdown:" + string(id))) // Ensure dropdown container always exists (create it if needed) - dropdownContainer := c.container(widgetID(dropdownID), 0) - // Start with the dropdown closed + // Handle delayed closing of dropdown + if dropdownContainer.closeDelay > 0 { + dropdownContainer.closeDelay-- + if dropdownContainer.closeDelay == 0 { + dropdownContainer.open = false + } + } + // Start with the dropdown closed if dropdownContainer.layout.Bounds.Empty() { dropdownContainer.open = false } @@ -98,11 +106,12 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E return e, err }) - // Handle option selection: update index and close dropdown + // Handle option selection: update index and start close delay if wasPressed { *selectedIndex = i if cnt := c.container(widgetID(dropdownID), 0); cnt != nil { - cnt.open = false // Close the dropdown when an option is selected + // Start the close delay timer (0.1 seconds at TPS rate) + cnt.closeDelay = ebiten.TPS() / 10 } } }) @@ -125,7 +134,10 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E clickInDropdown := clickPos.In(dropdownContainer.layout.Bounds) if !clickInButton && !clickInDropdown { - dropdownContainer.open = false + // Only close immediately if there's no close delay active + if dropdownContainer.closeDelay == 0 { + dropdownContainer.open = false + } } } @@ -136,14 +148,16 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E isOpen := dropdownContainer.open if isOpen { - // Close the dropdown + // Close the dropdown immediately and cancel any pending delay dropdownContainer.open = false + dropdownContainer.closeDelay = 0 } else { // Store the current state before opening, made in some desperate attempts to avoid feedback loops wasClosedBefore := !dropdownContainer.open - // Open the dropdown + // Open the dropdown and cancel any pending close delay dropdownContainer.open = true + dropdownContainer.closeDelay = 0 // Position dropdown directly below button with proper width if wasClosedBefore { From d1e6939b255409c067a8ae5aeee2a1d58d75d205 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 <50602525+Vmarcelo49@users.noreply.github.com> Date: Wed, 2 Jul 2025 07:47:26 -0300 Subject: [PATCH 04/12] Fixing wrong math --- dropdown.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dropdown.go b/dropdown.go index b774803..744df7f 100644 --- a/dropdown.go +++ b/dropdown.go @@ -159,15 +159,18 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E dropdownContainer.open = true dropdownContainer.closeDelay = 0 - // Position dropdown directly below button with proper width if wasClosedBefore { dropdownPos := image.Pt(bounds.Min.X, bounds.Max.Y) buttonWidth := bounds.Dx() - optionHeight := c.style().defaultHeight + c.style().padding - estimatedHeight := len(options) * optionHeight + optionHeight := c.style().defaultHeight + c.style().padding + 1 + totalHeight := len(options) * optionHeight + + maxDropdownHeight := c.style().defaultHeight * 12 // around 10 items visible? + actualHeight := min(totalHeight, maxDropdownHeight) + dropdownContainer.layout.Bounds = image.Rectangle{ Min: dropdownPos, - Max: dropdownPos.Add(image.Pt(buttonWidth, estimatedHeight)), + Max: dropdownPos.Add(image.Pt(buttonWidth, actualHeight)), } } } From d6d6b5902cd10830b6b8f41f2a65af424c03624b Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Wed, 2 Jul 2025 11:13:44 -0300 Subject: [PATCH 05/12] Put one example on the gallery and removed the Dropdown type --- container.go | 4 +- dropdown.go | 31 +++++---- example/dropdown/main.go | 131 --------------------------------------- example/gallery/main.go | 4 ++ example/gallery/ui.go | 16 +++++ 5 files changed, 36 insertions(+), 150 deletions(-) delete mode 100644 example/dropdown/main.go diff --git a/container.go b/container.go index 6536ade..93361b3 100644 --- a/container.go +++ b/container.go @@ -24,8 +24,8 @@ type container struct { toggledIDs map[widgetID]struct{} textInputTextFields map[widgetID]*textinput.Field - // closeDelay is used for delayed closing of dropdowns - closeDelay int + // dropdownCloseDelay is used for delayed closing of dropdowns + dropdownCloseDelay int used bool } diff --git a/dropdown.go b/dropdown.go index 744df7f..a2c2923 100644 --- a/dropdown.go +++ b/dropdown.go @@ -9,9 +9,6 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -// DropdownID is the ID of a dropdown menu container. -type DropdownID widgetID - // Dropdown creates a dropdown menu widget that allows users to select from a list of options. // selectedIndex is a pointer to the currently selected option index (0-based). // options is a slice of strings representing the available choices. @@ -35,15 +32,15 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E } last := *selectedIndex - dropdownID := DropdownID(c.idFromString("dropdown:" + string(id))) + dropdownID := c.idFromString("dropdown:" + string(id)) // Ensure dropdown container always exists (create it if needed) - dropdownContainer := c.container(widgetID(dropdownID), 0) + dropdownContainer := c.container(dropdownID, 0) // Handle delayed closing of dropdown - if dropdownContainer.closeDelay > 0 { - dropdownContainer.closeDelay-- - if dropdownContainer.closeDelay == 0 { + if dropdownContainer.dropdownCloseDelay > 0 { + dropdownContainer.dropdownCloseDelay-- + if dropdownContainer.dropdownCloseDelay == 0 { dropdownContainer.open = false } } @@ -56,9 +53,9 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E _ = c.wrapEventHandlerAndError(func() (EventHandler, error) { windowOptions := optionDropdown | optionNoResize | optionNoTitle - if err := c.window("", image.Rectangle{}, windowOptions, widgetID(dropdownID), func(layout ContainerLayout) { + if err := c.window("", image.Rectangle{}, windowOptions, dropdownID, func(layout ContainerLayout) { // Ensure dropdown container reference is fresh for each render - if cnt := c.container(widgetID(dropdownID), 0); cnt != nil { + if cnt := c.container(dropdownID, 0); cnt != nil { if cnt.open { c.bringToFront(cnt) } @@ -109,9 +106,9 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E // Handle option selection: update index and start close delay if wasPressed { *selectedIndex = i - if cnt := c.container(widgetID(dropdownID), 0); cnt != nil { + if cnt := c.container(dropdownID, 0); cnt != nil { // Start the close delay timer (0.1 seconds at TPS rate) - cnt.closeDelay = ebiten.TPS() / 10 + cnt.dropdownCloseDelay = ebiten.TPS() / 10 } } }) @@ -126,7 +123,7 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E return c.widget(id, optionAlignCenter, nil, func(bounds image.Rectangle, wasFocused bool) EventHandler { var e EventHandler - dropdownContainer := c.container(widgetID(dropdownID), 0) + dropdownContainer := c.container(dropdownID, 0) // Manual "click outside to close" and dropdown toggle, trying to do this in the container.go had lots of issues if dropdownContainer.open && c.pointing.justPressed() { clickPos := c.pointingPosition() @@ -135,7 +132,7 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E if !clickInButton && !clickInDropdown { // Only close immediately if there's no close delay active - if dropdownContainer.closeDelay == 0 { + if dropdownContainer.dropdownCloseDelay == 0 { dropdownContainer.open = false } } @@ -150,14 +147,14 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E if isOpen { // Close the dropdown immediately and cancel any pending delay dropdownContainer.open = false - dropdownContainer.closeDelay = 0 + dropdownContainer.dropdownCloseDelay = 0 } else { // Store the current state before opening, made in some desperate attempts to avoid feedback loops wasClosedBefore := !dropdownContainer.open // Open the dropdown and cancel any pending close delay dropdownContainer.open = true - dropdownContainer.closeDelay = 0 + dropdownContainer.dropdownCloseDelay = 0 if wasClosedBefore { dropdownPos := image.Pt(bounds.Min.X, bounds.Max.Y) @@ -195,7 +192,7 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E // Draw dropdown arrow indicator (up/down based on current state) arrowBounds := image.Rect(bounds.Max.X-arrowWidth, bounds.Min.Y, bounds.Max.X, bounds.Max.Y) icon := iconDown - if c.container(widgetID(dropdownID), 0).open { + if c.container(dropdownID, 0).open { icon = iconUp } c.drawIcon(icon, arrowBounds, c.style().colors[colorText]) diff --git a/example/dropdown/main.go b/example/dropdown/main.go deleted file mode 100644 index f1d5072..0000000 --- a/example/dropdown/main.go +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2025 The Ebitengine Authors - -package main - -import ( - "fmt" - "image" - "log" - "os" - - "github.com/ebitengine/debugui" - "github.com/hajimehoshi/ebiten/v2" -) - -type Game struct { - debugUI *debugui.DebugUI - - // Dropdown demo data - resolutionOptions []string - selectedResolution int - - qualityOptions []string - selectedQuality int - - nameOptions []string - selectedName int - - // Log for events - logBuffer []string -} - -func NewGame() *Game { - return &Game{ - debugUI: &debugui.DebugUI{}, - - resolutionOptions: []string{"720p", "1080p", "1440p", "4K", "Can it run Crysis?"}, - selectedResolution: 1, // Default to 1080p - - qualityOptions: []string{"Low", "Medium", "High", "Ultra"}, - selectedQuality: 2, // Default to High - - nameOptions: []string{"Alice", "Bob", "Charlie", "David", "Emma", "Frank", "Gopher", "Hajime", "Isabella", "Jack"}, - selectedName: 6, // Default to Gopher - - logBuffer: []string{}, - } -} - -func (g *Game) addLog(message string) { - g.logBuffer = append(g.logBuffer, message) - // Keep only last 10 messages - if len(g.logBuffer) > 10 { - g.logBuffer = g.logBuffer[1:] - } -} - -func (g *Game) Update() error { - _, err := g.debugUI.Update(func(ctx *debugui.Context) error { - // Main demo window - ctx.Window("Simple Dropdown Demo", image.Rect(50, 50, 400, 280), func(layout debugui.ContainerLayout) { - ctx.Header("Settings", true, func() { - ctx.SetGridLayout([]int{100, -1}, nil) - - // Resolution dropdown - ctx.Text("Resolution:") - ctx.Dropdown(&g.selectedResolution, g.resolutionOptions).On(func() { - g.addLog(fmt.Sprintf("Resolution: %s", g.resolutionOptions[g.selectedResolution])) - }) - - // Quality dropdown - ctx.Text("Quality:") - ctx.Dropdown(&g.selectedQuality, g.qualityOptions).On(func() { - g.addLog(fmt.Sprintf("Quality: %s", g.qualityOptions[g.selectedQuality])) - }) - ctx.Text("Reset Quality:") - ctx.Button("Reset").On(func() { // used to debug the dropdown above, clicking low also clicks on this - g.selectedQuality = 2 // Reset to High - }) - // Name dropdown - ctx.Text("Name:") - ctx.Dropdown(&g.selectedName, g.nameOptions).On(func() { - g.addLog(fmt.Sprintf("Name: %s", g.nameOptions[g.selectedName])) - }) - }) - - ctx.Header("Current Selection", false, func() { - ctx.Text(fmt.Sprintf("Resolution: %s", g.resolutionOptions[g.selectedResolution])) - ctx.Text(fmt.Sprintf("Quality: %s", g.qualityOptions[g.selectedQuality])) - ctx.Text(fmt.Sprintf("Name: %s", g.nameOptions[g.selectedName])) - }) - - ctx.Header("Actions", false, func() { - ctx.SetGridLayout([]int{-1, -1}, nil) - ctx.Button("Reset Settings").On(func() { - g.selectedResolution = 1 - g.selectedQuality = 2 - g.selectedName = 6 - g.addLog("Settings reset to defaults") - }) - ctx.Button("Clear Log").On(func() { - g.logBuffer = []string{} - }) - }) - }) - - return nil - }) - - return err -} - -func (g *Game) Draw(screen *ebiten.Image) { - g.debugUI.Draw(screen) -} - -func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { - return outsideWidth, outsideHeight -} - -func main() { - ebiten.SetWindowTitle("Simple Dropdown Demo") - ebiten.SetWindowSize(800, 600) - ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) - - game := NewGame() - if err := ebiten.RunGame(game); err != nil { - log.Fatal(err) - os.Exit(1) - } -} diff --git a/example/gallery/main.go b/example/gallery/main.go index bf19dd4..90a74e3 100644 --- a/example/gallery/main.go +++ b/example/gallery/main.go @@ -48,6 +48,9 @@ type Game struct { num3_2 float64 num4 float64 num5 int + + selectedOption, anotherSelectedOption int + dropdownOptions, anotherDropdownOptions []string } func NewGame() (*Game, error) { @@ -102,6 +105,7 @@ func (g *Game) Update() error { g.testWindow(ctx) g.logWindow(ctx) g.buttonWindows(ctx) + g.dropdownWindows(ctx) return nil }) if err != nil { diff --git a/example/gallery/ui.go b/example/gallery/ui.go index 66e6a8a..5c12844 100644 --- a/example/gallery/ui.go +++ b/example/gallery/ui.go @@ -211,3 +211,19 @@ func (g *Game) buttonWindows(ctx *debugui.Context) { } }) } + +func (g *Game) dropdownWindows(ctx *debugui.Context) { + g.dropdownOptions = []string{"Option 1", "Option 2", "Option 3", "Option 4", "Option 5"} + g.anotherDropdownOptions = []string{"Choice A", "Choice B", "Choice C", "Choice D", "Choice E"} + ctx.Window("Dropdown Windows", image.Rect(670, 40, 950, 290), func(layout debugui.ContainerLayout) { + ctx.Text("Select an option:") + ctx.Dropdown(&g.selectedOption, g.dropdownOptions).On(func() { + g.writeLog(fmt.Sprintf("Selected option: %s", g.dropdownOptions[g.selectedOption])) + }) + ctx.Text("Another dropdown:") + ctx.Dropdown(&g.anotherSelectedOption, g.anotherDropdownOptions).On(func() { + g.writeLog(fmt.Sprintf("Selected another option: %s", g.anotherDropdownOptions[g.anotherSelectedOption])) + }) + }) + +} From cbb7089b8e13d164df33d803e0e5cb4baffeb215 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 <50602525+Vmarcelo49@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:10:22 -0300 Subject: [PATCH 06/12] Changed to a header instead of a new window --- example/gallery/ui.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/example/gallery/ui.go b/example/gallery/ui.go index 5c12844..d2ebaa4 100644 --- a/example/gallery/ui.go +++ b/example/gallery/ui.go @@ -65,6 +65,20 @@ func (g *Game) testWindow(ctx *debugui.Context) { ctx.OpenPopup(popupID) }) }) + g.dropdownOptions = []string{"Option 1", "Option 2", "Option 3", "Option 4", "Option 5"} + g.anotherDropdownOptions = []string{"Choice A", "Choice B", "Choice C", "Choice D", "Choice E"} + ctx.Header("Dropdown Menu", true, func() { + ctx.SetGridLayout([]int{-1, -1}, nil) + ctx.Text("Select an option:") + ctx.Dropdown(&g.selectedOption, g.dropdownOptions).On(func() { + g.writeLog(fmt.Sprintf("Selected option: %s", g.dropdownOptions[g.selectedOption])) + }) + ctx.Text("Another dropdown:") + ctx.Dropdown(&g.anotherSelectedOption, g.anotherDropdownOptions).On(func() { + g.writeLog(fmt.Sprintf("Selected another option: %s", g.anotherDropdownOptions[g.anotherSelectedOption])) + }) + }) + ctx.Header("Tree and Text", true, func() { ctx.SetGridLayout([]int{-1, -1}, nil) ctx.GridCell(func(bounds image.Rectangle) { @@ -213,17 +227,5 @@ func (g *Game) buttonWindows(ctx *debugui.Context) { } func (g *Game) dropdownWindows(ctx *debugui.Context) { - g.dropdownOptions = []string{"Option 1", "Option 2", "Option 3", "Option 4", "Option 5"} - g.anotherDropdownOptions = []string{"Choice A", "Choice B", "Choice C", "Choice D", "Choice E"} - ctx.Window("Dropdown Windows", image.Rect(670, 40, 950, 290), func(layout debugui.ContainerLayout) { - ctx.Text("Select an option:") - ctx.Dropdown(&g.selectedOption, g.dropdownOptions).On(func() { - g.writeLog(fmt.Sprintf("Selected option: %s", g.dropdownOptions[g.selectedOption])) - }) - ctx.Text("Another dropdown:") - ctx.Dropdown(&g.anotherSelectedOption, g.anotherDropdownOptions).On(func() { - g.writeLog(fmt.Sprintf("Selected another option: %s", g.anotherDropdownOptions[g.anotherSelectedOption])) - }) - }) } From 9d28e7d89668a0d883f9ab3adffb8d16dc4ce4e6 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 <50602525+Vmarcelo49@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:14:41 -0300 Subject: [PATCH 07/12] oops forgot to remove that func --- example/gallery/main.go | 1 - example/gallery/ui.go | 4 ---- 2 files changed, 5 deletions(-) diff --git a/example/gallery/main.go b/example/gallery/main.go index 90a74e3..fa05f4d 100644 --- a/example/gallery/main.go +++ b/example/gallery/main.go @@ -105,7 +105,6 @@ func (g *Game) Update() error { g.testWindow(ctx) g.logWindow(ctx) g.buttonWindows(ctx) - g.dropdownWindows(ctx) return nil }) if err != nil { diff --git a/example/gallery/ui.go b/example/gallery/ui.go index d2ebaa4..d5da90a 100644 --- a/example/gallery/ui.go +++ b/example/gallery/ui.go @@ -225,7 +225,3 @@ func (g *Game) buttonWindows(ctx *debugui.Context) { } }) } - -func (g *Game) dropdownWindows(ctx *debugui.Context) { - -} From bb30eb6474d028150511766809daa53bda27895a Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Thu, 3 Jul 2025 09:10:57 -0300 Subject: [PATCH 08/12] Correcting wrong licence Removed some obvious comments, keeping only the specific ones about the dropdowns removed isOpen variable that was used only once --- dropdown.go | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/dropdown.go b/dropdown.go index a2c2923..4b4e24e 100644 --- a/dropdown.go +++ b/dropdown.go @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2025 The Ebitengine Authors package debugui @@ -23,10 +23,8 @@ func (c *Context) Dropdown(selectedIndex *int, options []string) EventHandler { func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (EventHandler, error) { if selectedIndex == nil || len(options) == 0 { - // If no options or selectedIndex is nil, return a null event handler return &nullEventHandler{}, nil } - // Clamp selectedIndex to valid range to prevent out-of-bounds access if *selectedIndex < 0 || *selectedIndex >= len(options) { *selectedIndex = 0 } @@ -34,7 +32,6 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E dropdownID := c.idFromString("dropdown:" + string(id)) - // Ensure dropdown container always exists (create it if needed) dropdownContainer := c.container(dropdownID, 0) // Handle delayed closing of dropdown @@ -45,7 +42,6 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E } } - // Start with the dropdown closed if dropdownContainer.layout.Bounds.Empty() { dropdownContainer.open = false } @@ -54,22 +50,17 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E windowOptions := optionDropdown | optionNoResize | optionNoTitle if err := c.window("", image.Rectangle{}, windowOptions, dropdownID, func(layout ContainerLayout) { - // Ensure dropdown container reference is fresh for each render if cnt := c.container(dropdownID, 0); cnt != nil { if cnt.open { c.bringToFront(cnt) } } - - // full width dropdown c.SetGridLayout([]int{-1}, nil) - // Render each dropdown option as a clickable button for i, option := range options { c.IDScope(option, func() { isSelected := i == *selectedIndex - // Highlight the currently selected option var buttonColor int if isSelected { buttonColor = colorButtonFocus @@ -77,7 +68,6 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E buttonColor = colorButton } - // Create clickable button widget for this option pc := caller() buttonID := c.idFromCaller(pc) var wasPressed bool @@ -87,14 +77,12 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E var e EventHandler if c.pointing.justPressed() && c.focus == buttonID { - // Handle option selection wasPressed = true e = &eventHandler{} } return e }, func(bounds image.Rectangle) { - // Draw the option button with appropriate styling c.drawWidgetFrame(buttonID, bounds, buttonColor, optionAlignCenter) if len(option) > 0 { c.drawWidgetText(option, bounds, colorText, optionAlignCenter) @@ -119,7 +107,6 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E return nil, nil }) - // Create the main dropdown button that toggles the menu return c.widget(id, optionAlignCenter, nil, func(bounds image.Rectangle, wasFocused bool) EventHandler { var e EventHandler @@ -138,18 +125,12 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E } } - // Toggle dropdown when button is clicked if c.pointing.justPressed() && c.focus == id { - // Check if dropdown container exists and its state - - isOpen := dropdownContainer.open - - if isOpen { + if dropdownContainer.open { // Close the dropdown immediately and cancel any pending delay dropdownContainer.open = false dropdownContainer.dropdownCloseDelay = 0 } else { - // Store the current state before opening, made in some desperate attempts to avoid feedback loops wasClosedBefore := !dropdownContainer.open // Open the dropdown and cancel any pending close delay @@ -172,24 +153,19 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E } } } - - // Generate event if user changed selection if last != *selectedIndex { e = &eventHandler{} } return e }, func(bounds image.Rectangle) { - // Draw the dropdown button appearance c.drawWidgetFrame(id, bounds, colorButton, optionAlignCenter) - // Show currently selected text (reserve space for arrow - use widget height for square arrow area) arrowWidth := bounds.Dy() textBounds := bounds textBounds.Max.X -= arrowWidth c.drawWidgetText(options[*selectedIndex], textBounds, colorText, optionAlignCenter) - // Draw dropdown arrow indicator (up/down based on current state) arrowBounds := image.Rect(bounds.Max.X-arrowWidth, bounds.Min.Y, bounds.Max.X, bounds.Max.Y) icon := iconDown if c.container(dropdownID, 0).open { From 68850baf3c024da4176908df798a6bb1425e8370 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Thu, 3 Jul 2025 10:56:11 -0300 Subject: [PATCH 09/12] Solving issue with duplicate names in the options --- dropdown.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dropdown.go b/dropdown.go index 4b4e24e..6240ca2 100644 --- a/dropdown.go +++ b/dropdown.go @@ -5,6 +5,7 @@ package debugui import ( "image" + "strconv" "github.com/hajimehoshi/ebiten/v2" ) @@ -58,7 +59,7 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E c.SetGridLayout([]int{-1}, nil) for i, option := range options { - c.IDScope(option, func() { + c.IDScope(strconv.Itoa(i), func() { isSelected := i == *selectedIndex var buttonColor int From eaaa6cdefbe37e1b4e7587737d21f8e9a2706b24 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Thu, 3 Jul 2025 11:01:14 -0300 Subject: [PATCH 10/12] renaming variables inside the game struct to match the pattern --- example/gallery/main.go | 4 ++-- example/gallery/ui.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/example/gallery/main.go b/example/gallery/main.go index fa05f4d..6dca544 100644 --- a/example/gallery/main.go +++ b/example/gallery/main.go @@ -49,8 +49,8 @@ type Game struct { num4 float64 num5 int - selectedOption, anotherSelectedOption int - dropdownOptions, anotherDropdownOptions []string + selectedOption1, selectedOption2 int + dropdownOptions1, dropdownOptions2 []string } func NewGame() (*Game, error) { diff --git a/example/gallery/ui.go b/example/gallery/ui.go index d5da90a..c58aaf0 100644 --- a/example/gallery/ui.go +++ b/example/gallery/ui.go @@ -65,17 +65,17 @@ func (g *Game) testWindow(ctx *debugui.Context) { ctx.OpenPopup(popupID) }) }) - g.dropdownOptions = []string{"Option 1", "Option 2", "Option 3", "Option 4", "Option 5"} - g.anotherDropdownOptions = []string{"Choice A", "Choice B", "Choice C", "Choice D", "Choice E"} + g.dropdownOptions1 = []string{"Option 1", "Option 2", "Option 3", "Option 4", "Option 5"} + g.dropdownOptions2 = []string{"Choice A", "Choice B", "Choice C", "Choice D", "Choice E"} ctx.Header("Dropdown Menu", true, func() { ctx.SetGridLayout([]int{-1, -1}, nil) ctx.Text("Select an option:") - ctx.Dropdown(&g.selectedOption, g.dropdownOptions).On(func() { - g.writeLog(fmt.Sprintf("Selected option: %s", g.dropdownOptions[g.selectedOption])) + ctx.Dropdown(&g.selectedOption1, g.dropdownOptions1).On(func() { + g.writeLog(fmt.Sprintf("Selected option: %s", g.dropdownOptions1[g.selectedOption1])) }) ctx.Text("Another dropdown:") - ctx.Dropdown(&g.anotherSelectedOption, g.anotherDropdownOptions).On(func() { - g.writeLog(fmt.Sprintf("Selected another option: %s", g.anotherDropdownOptions[g.anotherSelectedOption])) + ctx.Dropdown(&g.selectedOption2, g.dropdownOptions2).On(func() { + g.writeLog(fmt.Sprintf("Selected another option: %s", g.dropdownOptions2[g.selectedOption2])) }) }) From 1f1574422db44eaf8149358ae48df8a7a48c36e3 Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Thu, 3 Jul 2025 11:13:07 -0300 Subject: [PATCH 11/12] Removing optionDropdown since its unused --- dropdown.go | 2 +- widget.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dropdown.go b/dropdown.go index 6240ca2..84fb310 100644 --- a/dropdown.go +++ b/dropdown.go @@ -48,7 +48,7 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E } _ = c.wrapEventHandlerAndError(func() (EventHandler, error) { - windowOptions := optionDropdown | optionNoResize | optionNoTitle + windowOptions := optionNoResize | optionNoTitle if err := c.window("", image.Rectangle{}, windowOptions, dropdownID, func(layout ContainerLayout) { if cnt := c.container(dropdownID, 0); cnt != nil { diff --git a/widget.go b/widget.go index 5e9a770..1d03c6a 100644 --- a/widget.go +++ b/widget.go @@ -28,7 +28,6 @@ const ( optionHoldFocus optionAutoSize optionPopup - optionDropdown optionClosed optionExpanded ) From 6f35056090daf3f686b365864011e0ebfed86eab Mon Sep 17 00:00:00 2001 From: Vmarcelo49 Date: Thu, 3 Jul 2025 11:32:12 -0300 Subject: [PATCH 12/12] Using c.Button instead of re creating the button code logic inside the dropdown code --- dropdown.go | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/dropdown.go b/dropdown.go index 84fb310..0ae4ede 100644 --- a/dropdown.go +++ b/dropdown.go @@ -60,46 +60,13 @@ func (c *Context) dropdown(selectedIndex *int, options []string, id widgetID) (E for i, option := range options { c.IDScope(strconv.Itoa(i), func() { - isSelected := i == *selectedIndex - - var buttonColor int - if isSelected { - buttonColor = colorButtonFocus - } else { - buttonColor = colorButton - } - - pc := caller() - buttonID := c.idFromCaller(pc) - var wasPressed bool - - _ = c.wrapEventHandlerAndError(func() (EventHandler, error) { - e, err := c.widget(buttonID, optionAlignCenter, nil, func(bounds image.Rectangle, wasFocused bool) EventHandler { - var e EventHandler - - if c.pointing.justPressed() && c.focus == buttonID { - wasPressed = true - e = &eventHandler{} - } - - return e - }, func(bounds image.Rectangle) { - c.drawWidgetFrame(buttonID, bounds, buttonColor, optionAlignCenter) - if len(option) > 0 { - c.drawWidgetText(option, bounds, colorText, optionAlignCenter) - } - }) - return e, err - }) - - // Handle option selection: update index and start close delay - if wasPressed { + c.Button(option).On(func() { *selectedIndex = i if cnt := c.container(dropdownID, 0); cnt != nil { // Start the close delay timer (0.1 seconds at TPS rate) cnt.dropdownCloseDelay = ebiten.TPS() / 10 } - } + }) }) } }); err != nil {