Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that popups and menus fit within the screen bounds for mobile #1400

Merged
merged 5 commits into from Oct 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions canvas.go
Expand Up @@ -53,4 +53,8 @@ type Canvas interface {
// PixelCoordinateForPosition returns the x and y pixel coordinate for a given position on this canvas.
// This can be used to find absolute pixel positions or pixel offsets relative to an object top left.
PixelCoordinateForPosition(Position) (int, int)

// InteractiveArea returns the position and size of the central interactive area.
// Operating system elements may overlap the portions outside this area and widgets should avoid being outside.
InteractiveArea() (Position, Size)
}
5 changes: 5 additions & 0 deletions internal/driver/glfw/canvas.go
Expand Up @@ -88,6 +88,10 @@ func (c *glCanvas) FocusPrevious() {
c.focusManager().FocusPrevious()
}

func (c *glCanvas) InteractiveArea() (fyne.Position, fyne.Size) {
return fyne.Position{}, c.Size()
}

func (c *glCanvas) MinSize() fyne.Size {
c.RLock()
defer c.RUnlock()
Expand Down Expand Up @@ -602,6 +606,7 @@ func newCanvas() *glCanvas {
c.overlays = &overlayStack{
OverlayStack: internal.OverlayStack{
OnChange: c.overlayChanged,
Canvas: c,
},
}

Expand Down
49 changes: 24 additions & 25 deletions internal/driver/gomobile/canvas.go
Expand Up @@ -53,10 +53,22 @@ func (c *mobileCanvas) Content() fyne.CanvasObject {
return c.content
}

func (c *mobileCanvas) InteractiveArea() (fyne.Position, fyne.Size) {
scale := fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile

dev, ok := fyne.CurrentDevice().(*device)
if !ok || dev.safeWidth == 0 || dev.safeHeight == 0 {
return fyne.NewPos(0, 0), c.Size() // running in test mode
}

return fyne.NewPos(int(float32(dev.safeLeft)/scale), int(float32(dev.safeTop)/scale)),
fyne.NewSize(int(float32(dev.safeWidth)/scale), int(float32(dev.safeHeight)/scale))
}

func (c *mobileCanvas) SetContent(content fyne.CanvasObject) {
c.content = content

c.sizeContent(c.Size().Union(content.MinSize()))
c.sizeContent(c.Size().Max(content.MinSize()))
}

// Deprecated: Use Overlays() instead.
Expand Down Expand Up @@ -85,58 +97,45 @@ func (c *mobileCanvas) Refresh(obj fyne.CanvasObject) {
}
}

func (c *mobileCanvas) edgePadding() (topLeft, bottomRight fyne.Size) {
scale := fyne.CurrentDevice().SystemScaleForWindow(nil) // we don't need a window parameter on mobile

dev, ok := fyne.CurrentDevice().(*device)
if !ok {
return fyne.NewSize(0, 0), fyne.NewSize(0, 0) // running in test mode
}

return fyne.NewSize(int(float32(dev.insetLeft)/scale), int(float32(dev.insetTop)/scale)),
fyne.NewSize(int(float32(dev.insetRight)/scale), int(float32(dev.insetBottom)/scale))
}

func (c *mobileCanvas) sizeContent(size fyne.Size) {
if c.content == nil { // window may not be configured yet
return
}
c.size = size

offset := fyne.NewPos(0, 0)
devicePadTopLeft, devicePadBottomRight := c.edgePadding()
areaPos, areaSize := c.InteractiveArea()

if c.windowHead != nil {
topHeight := c.windowHead.MinSize().Height

if len(c.windowHead.(*widget.Box).Children) > 1 {
c.windowHead.Resize(fyne.NewSize(size.Width-devicePadTopLeft.Width-devicePadBottomRight.Width, topHeight))
c.windowHead.Resize(fyne.NewSize(areaSize.Width, topHeight))
offset = fyne.NewPos(0, topHeight)
} else {
c.windowHead.Resize(c.windowHead.MinSize())
}
c.windowHead.Move(fyne.NewPos(devicePadTopLeft.Width, devicePadTopLeft.Height))
c.windowHead.Move(areaPos)
}

innerSize := size.Subtract(devicePadTopLeft).Subtract(devicePadBottomRight)
topLeft := offset.Add(fyne.NewPos(devicePadTopLeft.Width, devicePadTopLeft.Height))

c.size = size
topLeft := areaPos.Add(offset)
for _, overlay := range c.overlays.List() {
if p, ok := overlay.(*widget.PopUp); ok {
// TODO: remove this when #707 is being addressed.
// “Notifies” the PopUp of the canvas size change.
size := p.Content.Size().Add(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)).Min(innerSize)
size := p.Content.Size().Add(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)).Min(areaSize)
p.Resize(size)
} else {
overlay.Resize(innerSize)
overlay.Resize(areaSize)
overlay.Move(topLeft)
}
}

if c.padded {
c.content.Resize(innerSize.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)))
c.content.Resize(areaSize.Subtract(fyne.NewSize(theme.Padding()*2, theme.Padding()*2)))
c.content.Move(topLeft.Add(fyne.NewPos(theme.Padding(), theme.Padding())))
} else {
c.content.Resize(innerSize)
c.content.Resize(areaSize)
c.content.Move(topLeft)
}
}
Expand Down Expand Up @@ -529,7 +528,7 @@ func NewCanvas() fyne.Canvas {
ret.lastTapDownPos = make(map[int]fyne.Position)
ret.lastTapDown = make(map[int]time.Time)
ret.minSizeCache = make(map[fyne.CanvasObject]fyne.Size)
ret.overlays = &internal.OverlayStack{}
ret.overlays = &internal.OverlayStack{Canvas: ret}

ret.setupThemeListener()

Expand Down
2 changes: 1 addition & 1 deletion internal/driver/gomobile/device.go
Expand Up @@ -8,7 +8,7 @@ import (
)

type device struct {
insetTop, insetBottom, insetLeft, insetRight int
safeTop, safeLeft, safeWidth, safeHeight int
}

//lint:file-ignore U1000 Var currentDPI is used in other files, but not here
Expand Down
8 changes: 4 additions & 4 deletions internal/driver/gomobile/driver.go
Expand Up @@ -125,10 +125,10 @@ func (d *mobileDriver) Run() {
currentDPI = e.PixelsPerPt * 72

dev := d.device
dev.insetTop = e.InsetTopPx
dev.insetBottom = e.InsetBottomPx
dev.insetLeft = e.InsetLeftPx
dev.insetRight = e.InsetRightPx
dev.safeTop = e.InsetTopPx
dev.safeLeft = e.InsetLeftPx
dev.safeHeight = e.HeightPx - e.InsetTopPx - e.InsetBottomPx
dev.safeWidth = e.WidthPx - e.InsetLeftPx - e.InsetRightPx
canvas.SetScale(0) // value is ignored

// make sure that we paint on the next frame
Expand Down
11 changes: 5 additions & 6 deletions internal/driver/gomobile/menu.go
Expand Up @@ -59,12 +59,11 @@ func (c *mobileCanvas) showMenu(menu *fyne.MainMenu) {
shadow := canvas.NewHorizontalGradient(theme.ShadowColor(), color.Transparent)
c.menu = fyne.NewContainer(panel, shadow)

devicePadTopLeft, devicePadBottomRight := c.edgePadding()
padY := devicePadTopLeft.Height + devicePadBottomRight.Height
panel.Move(fyne.NewPos(devicePadTopLeft.Width, devicePadTopLeft.Height))
panel.Resize(fyne.NewSize(panel.MinSize().Width+theme.Padding(), c.size.Height-padY))
shadow.Resize(fyne.NewSize(theme.Padding()/2, c.size.Height-padY))
shadow.Move(fyne.NewPos(panel.Size().Width+devicePadTopLeft.Width, devicePadTopLeft.Height))
safePos, safeSize := c.InteractiveArea()
panel.Move(safePos)
panel.Resize(fyne.NewSize(panel.MinSize().Width+theme.Padding(), safeSize.Height))
shadow.Resize(fyne.NewSize(theme.Padding()/2, safeSize.Height))
shadow.Move(fyne.NewPos(panel.Size().Width+safePos.X, safePos.Y))
}

func (d *mobileDriver) findMenu(win *window) *fyne.MainMenu {
Expand Down
11 changes: 11 additions & 0 deletions internal/overlay_stack.go
Expand Up @@ -5,11 +5,13 @@ import (

"fyne.io/fyne"
"fyne.io/fyne/internal/app"
"fyne.io/fyne/internal/widget"
)

// OverlayStack implements fyne.OverlayStack
type OverlayStack struct {
OnChange func()
Canvas fyne.Canvas
focusManagers []*app.FocusManager
overlays []fyne.CanvasObject
propertyLock sync.RWMutex
Expand All @@ -28,6 +30,15 @@ func (s *OverlayStack) Add(overlay fyne.CanvasObject) {
return
}
s.overlays = append(s.overlays, overlay)

// TODO this should probably apply to all once #707 is addressed
if _, ok := overlay.(*widget.OverlayContainer); ok {
safePos, safeSize := s.Canvas.InteractiveArea()

overlay.Resize(safeSize)
overlay.Move(safePos)
}

s.focusManagers = append(s.focusManagers, app.NewFocusManager(overlay))
if s.OnChange != nil {
s.OnChange()
Expand Down
3 changes: 2 additions & 1 deletion internal/overlay_stack_test.go
Expand Up @@ -7,11 +7,12 @@ import (

"fyne.io/fyne"
"fyne.io/fyne/internal"
"fyne.io/fyne/test"
"fyne.io/fyne/widget"
)

func TestOverlayStack(t *testing.T) {
s := &internal.OverlayStack{}
s := &internal.OverlayStack{Canvas: test.NewCanvas()}
o1 := widget.NewLabel("A")
o2 := widget.NewLabel("B")
o3 := widget.NewLabel("C")
Expand Down
9 changes: 7 additions & 2 deletions test/testcanvas.go
Expand Up @@ -55,13 +55,14 @@ func Canvas() fyne.Canvas {

// NewCanvas returns a single use in-memory canvas used for testing
func NewCanvas() WindowlessCanvas {
return &testCanvas{
c := &testCanvas{
focusMgr: app.NewFocusManager(nil),
overlays: &internal.OverlayStack{},
padded: true,
scale: 1.0,
size: fyne.NewSize(10, 10),
}
c.overlays = &internal.OverlayStack{Canvas: c}
return c
}

// NewCanvasWithPainter allows creation of an in-memory canvas with a specific painter.
Expand Down Expand Up @@ -101,6 +102,10 @@ func (c *testCanvas) Focused() fyne.Focusable {
return c.focusManager().Focused()
}

func (c *testCanvas) InteractiveArea() (fyne.Position, fyne.Size) {
return fyne.Position{}, c.Size()
}

func (c *testCanvas) OnTypedKey() func(*fyne.KeyEvent) {
c.propertyLock.RLock()
defer c.propertyLock.RUnlock()
Expand Down
4 changes: 3 additions & 1 deletion widget/menu.go
Expand Up @@ -160,7 +160,9 @@ func (r *menuRenderer) Layout(s fyne.Size) {
scrollSize := boxSize
if c := fyne.CurrentApp().Driver().CanvasForObject(r.m); c != nil {
ap := fyne.CurrentApp().Driver().AbsolutePositionForObject(r.m)
if ah := c.Size().Height - ap.Y; ah < boxSize.Height {
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)
}
}
Expand Down