package ui import ( "log" "github.com/hajimehoshi/ebiten/v2" "golang.org/x/exp/slices" ) // Container to organise multiple Widgets // A collection does implement the Widget interface and thus can be nested in // another Collection. type Collection struct { Width, Height int hoverDetector hoverDetector widgets []Widget focused Widget } type CollectionInterface interface { Widget Widgets() []Widget Clear() AddWidget(Widget) FindWidget(Widget) int RemoveWidget(Widget) RemoveWidgetAt(int) } func (c *Collection) Layout() (int, int) { return c.Width, c.Height } func (c *Collection) Widgets() []Widget { return c.widgets } func (c *Collection) Clear() { c.widgets = []Widget{} } func (c *Collection) ResetHover() { c.hoverDetector.reset(0, 0) } func (c *Collection) AddWidget(w Widget) { if w == nil { log.Panicf("Adding nil widget to collection") } if slices.Contains(c.widgets, w) { log.Panicf("Double insertion of %v", w) } c.widgets = append(c.widgets, w) } func (c *Collection) FindWidget(toFind Widget) int { for i, w := range c.widgets { if w != toFind { continue } return i } return -1 } func (c *Collection) RemoveWidget(widget Widget) { idx := c.FindWidget(widget) if idx != -1 { c.RemoveWidgetAt(idx) } } func (c *Collection) RemoveWidgetAt(idx int) { nwidgets := len(c.widgets) c.widgets[idx] = c.widgets[nwidgets-1] c.widgets = c.widgets[:nwidgets-1] } func (c *Collection) Draw(screen *ebiten.Image) { for _, w := range c.widgets { w.Draw(screen) } } func (c *Collection) Contains(x, y int) bool { for _, w := range c.widgets { if w.Contains(x, y) { return true } } return false } func (c *Collection) FindWidgetAt(x, y int) Widget { // Iterate the widget in reverse order to ensure that upper widgets are // considered first for i := len(c.widgets) - 1; i >= 0; i-- { w := c.widgets[i] if !w.Contains(x, y) { continue } return w } return nil } func (c *Collection) FindObjectAt(x, y int) interface{} { // Iterate the widget in reverse order to ensure that upper widgets are // considered first for i := len(c.widgets) - 1; i >= 0; i-- { w := c.widgets[i] if o := w.FindObjectAt(x, y); o != nil { return o } } return nil } func (c *Collection) switchFocus(fcsbl Widget) { log.Printf("switch focus from %v to %v\n", c.focused, fcsbl) if c.focused != nil { c.focused.Focus(false) } if fcsbl != nil { fcsbl.Focus(true) c.focused = fcsbl } else { c.focused = nil } } func (c *Collection) handleTouchInputs() error { for i := len(TouchManager.Taps) - 1; i >= 0; i-- { tap := TouchManager.Taps[i] for i := len(c.widgets) - 1; i >= 0; i-- { w := c.widgets[i] if !w.Contains(tap.X, tap.Y) { continue } // Skip collections their widgets will receive left over input // during the following updateWidgets call, if _, ok := w.(CollectionInterface); ok { continue } if w.IsClickable() { w.Click(tap.X, tap.Y) } if w.IsFocusable() { c.switchFocus(w) } else if c.focused != nil { c.switchFocus(nil) } log.Printf("consume Tap for %T %v\n", w, w) TouchManager.consumeTap(i) break } } // TODO: handle pinching if TouchManager.Pan != nil { pan := TouchManager.Pan iX, iY := pan.Incremental() w := c.FindWidgetAt(int(pan.originX), (pan.originY)) if w != nil && w.IsScrollable() { w.Scroll(int(iX)/10, int(iY)/10) } } return nil } func passInput(ev InputEvent, textInput *TextInput) { textInput.AddInput(ev.ctx.(TextCtx).in) textInput.HandleKey() } func (c *Collection) IsUpdatable() bool { return true } func (c *Collection) updateWidgets() error { // Update all updatables for _, w := range c.widgets { if w.IsUpdatable() { if err := w.Update(); err != nil { return err } } } return nil } func (c *Collection) Update() error { defer c.updateWidgets() if err := c.handleTouchInputs(); err != nil { return err } for i := len(Input) - 1; i >= 0; i-- { ev := Input[i] w := c.FindWidgetAt(ev.x, ev.y) if w != nil { switch ev.kind { case Click: if w.IsClickable() && ev.ctx.(ClickCtx).btn == ebiten.MouseButtonLeft { w.Click(ev.x, ev.y) consumeInput(i) } // TODO: switch focus case Scroll: if w.IsScrollable() { s := ev.ctx.(ScrollCtx) w.Scroll(s.scrollX, s.scrollY) consumeInput(i) } case Text: if txt, ok := w.(*TextInput); ok { passInput(ev, txt) consumeInput(i) } } // handle focused text input } else if ev.kind == Text { if txt, ok := c.focused.(*TextInput); ok { passInput(ev, txt) consumeInput(i) } } } x, y := ebiten.CursorPosition() activeWidget := c.FindWidgetAt(x, y) c.hoverDetector.update(c, x, y, activeWidget) return nil } // The events for the contained widgets // are emitted during the collections // Update method. func (c *Collection) IsClickable() bool { return false } func (c *Collection) Click(int, int) {} func (c *Collection) IsScrollable() bool { return false } func (c *Collection) Scroll(int, int) {} func (c *Collection) IsFocusable() bool { return false } func (c *Collection) Focus(bool) {}