package widget
import (
"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/internal/widget"
"fyne.io/fyne/layout"
"fyne.io/fyne/theme"
)
var _ fyne.Widget = (*Menu)(nil)
var _ fyne.Tappable = (*Menu)(nil)
// Menu is a widget for displaying a fyne.Menu.
type Menu struct {
widget.Base
Items []fyne.CanvasObject
OnDismiss func()
activeItem *menuItem
customSized bool
}
// NewMenu creates a new Menu.
func NewMenu(menu *fyne.Menu) *Menu {
items := make([]fyne.CanvasObject, len(menu.Items))
m := &Menu{Items: items}
for i, item := range menu.Items {
if item.IsSeparator {
items[i] = NewSeparator()
} else {
items[i] = newMenuItem(item, m, m.activateChild)
}
}
return m
}
// CreateRenderer returns a new renderer for the menu.
//
// Implements: fyne.Widget
func (m *Menu) CreateRenderer() fyne.WidgetRenderer {
box := newMenuBox(m.Items)
scroll := NewVScrollContainer(box)
scroll.SetMinSize(box.MinSize())
objects := []fyne.CanvasObject{scroll}
for _, i := range m.Items {
if item, ok := i.(*menuItem); ok && item.Child() != nil {
objects = append(objects, item.Child())
}
}
return &menuRenderer{
widget.NewShadowingRenderer(objects, widget.MenuLevel),
box,
m,
scroll,
}
}
// DeactivateChild deactivates the active child menu.
func (m *Menu) DeactivateChild() {
if m.activeItem != nil {
m.activeItem.Child().Hide()
m.activeItem = nil
}
}
// Hide hides the menu.
//
// Implements: fyne.Widget
func (m *Menu) Hide() {
widget.HideWidget(&m.Base, m)
}
// MinSize returns the minimal size of the menu.
//
// Implements: fyne.Widget
func (m *Menu) MinSize() fyne.Size {
return widget.MinSizeOf(m)
}
// Move sets the position of the widget relative to its parent.
//
// Implements: fyne.Widget
func (m *Menu) Move(pos fyne.Position) {
widget.MoveWidget(&m.Base, m, pos)
}
// Refresh triggers a redraw of the menu.
//
// Implements: fyne.Widget
func (m *Menu) Refresh() {
widget.RefreshWidget(m)
}
// Resize has no effect because menus are always displayed with their minimal size.
//
// Implements: fyne.Widget
func (m *Menu) Resize(size fyne.Size) {
widget.ResizeWidget(&m.Base, m, size)
}
// Show makes the menu visible.
//
// Implements: fyne.Widget
func (m *Menu) Show() {
widget.ShowWidget(&m.Base, m)
}
// Tapped catches taps on separators and the menu background. It doesn’t perform any action.
//
// Implements: fyne.Tappable
func (m *Menu) Tapped(*fyne.PointEvent) {
// Hit a separator or padding -> do nothing.
}
// Dismiss dismisses the menu by dismissing and hiding the active child and performing OnDismiss.
func (m *Menu) Dismiss() {
if m.activeItem != nil {
defer m.activeItem.Child().Dismiss()
m.DeactivateChild()
}
if m.OnDismiss != nil {
m.OnDismiss()
}
}
func (m *Menu) activateChild(item *menuItem) {
if item.Child() != nil {
item.Child().DeactivateChild()
}
if m.activeItem == item {
return
}
m.DeactivateChild()
if item.Child() == nil {
return
}
m.activeItem = item
item.Child().Show()
m.Refresh()
}
type menuRenderer struct {
*widget.ShadowingRenderer
box *menuBox
m *Menu
scroll *ScrollContainer
}
func (r *menuRenderer) Layout(s fyne.Size) {
minSize := r.MinSize()
var boxSize fyne.Size
if r.m.customSized {
boxSize = minSize.Max(s)
} else {
boxSize = minSize
}
scrollSize := boxSize
if c := fyne.CurrentApp().Driver().CanvasForObject(r.m); c != nil {
ap := fyne.CurrentApp().Driver().AbsolutePositionForObject(r.m)
pos, size := c.InteractiveArea()
bottomPad := c.Size().Height - pos.Y - size.Height
if ah := c.Size().Height - bottomPad - ap.Y; ah < boxSize.Height {
scrollSize = fyne.NewSize(boxSize.Width, ah)
}
}
if scrollSize != r.m.Size() {
r.m.Resize(scrollSize)
return
}
r.LayoutShadow(scrollSize, fyne.NewPos(0, 0))
r.scroll.Resize(scrollSize)
r.box.Resize(boxSize)
r.layoutActiveChild()
}
func (r *menuRenderer) MinSize() fyne.Size {
return r.box.MinSize()
}
func (r *menuRenderer) Refresh() {
r.layoutActiveChild()
canvas.Refresh(r.m)
}
func (r *menuRenderer) layoutActiveChild() {
item := r.m.activeItem
if item == nil {
return
}
if item.Child().Size().IsZero() {
item.Child().Resize(item.Child().MinSize())
}
itemSize := item.Size()
cp := fyne.NewPos(itemSize.Width, item.Position().Y)
d := fyne.CurrentApp().Driver()
c := d.CanvasForObject(item)
if c != nil {
absPos := d.AbsolutePositionForObject(item)
childSize := item.Child().Size()
if absPos.X+itemSize.Width+childSize.Width > c.Size().Width {
if absPos.X-childSize.Width >= 0 {
cp.X = -childSize.Width
} else {
cp.X = c.Size().Width - absPos.X - childSize.Width
}
}
requiredHeight := childSize.Height - theme.Padding()
availableHeight := c.Size().Height - absPos.Y
missingHeight := requiredHeight - availableHeight
if missingHeight > 0 {
cp.Y -= missingHeight
}
}
item.Child().Move(cp)
}
type menuBox struct {
BaseWidget
items []fyne.CanvasObject
}
var _ fyne.Widget = (*menuBox)(nil)
func newMenuBox(items []fyne.CanvasObject) *menuBox {
b := &menuBox{items: items}
b.ExtendBaseWidget(b)
return b
}
func (b *menuBox) CreateRenderer() fyne.WidgetRenderer {
cont := fyne.NewContainerWithLayout(layout.NewVBoxLayout(), b.items...)
return &menuBoxRenderer{
BaseRenderer: widget.NewBaseRenderer([]fyne.CanvasObject{cont}),
b: b,
cont: cont,
}
}
type menuBoxRenderer struct {
widget.BaseRenderer
b *menuBox
cont *fyne.Container
}
var _ fyne.WidgetRenderer = (*menuBoxRenderer)(nil)
func (r *menuBoxRenderer) Layout(size fyne.Size) {
r.cont.Resize(fyne.NewSize(size.Width, size.Height+2*theme.Padding()))
r.cont.Move(fyne.NewPos(0, theme.Padding()))
}
func (r *menuBoxRenderer) MinSize() fyne.Size {
return r.cont.MinSize().Add(fyne.NewSize(0, 2*theme.Padding()))
}
func (r *menuBoxRenderer) Refresh() {
canvas.Refresh(r.b)
}