package ui import ( "log" "slices" "github.com/hajimehoshi/ebiten/v2" ) // 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 widgets []Widget focused Widget hovering 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) 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) } // MoveIdxToLast shifts the widget at index idx to the last position. func (c *Collection) MoveIdxToLast(idx int) { w := c.widgets[idx] after := c.widgets[idx+1:] c.widgets = c.widgets[:idx] c.widgets = append(c.widgets, after...) 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) any { // 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) 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() 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, Tap: // Only consider taps or left clicks if w.IsClickable() && (ev.Kind != Click || ev.Ctx.(ClickCtx).Btn == ebiten.MouseButtonLeft) { w.Click(ev.X, ev.Y) ConsumeInput(i) } if w.IsFocusable() && (ev.Kind != Click || ev.Ctx.(ClickCtx).Btn == ebiten.MouseButtonLeft) { c.switchFocus(w) } case Scroll, Pan: if w.IsScrollable() { s := ev.Ctx.(DistanceCtx) w.Scroll(s.scrollX, s.scrollY) ConsumeInput(i) } case Text: case HoverEnd: if c.hovering != nil { c.hovering.Hover(false, ev.X, ev.Y) c.hovering = nil } case HoverStart: if w.IsHoverable() { w.Hover(true, ev.X, ev.Y) c.hovering = w } } } } 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) {}