Skip to content

Commit

Permalink
Ensure that popups and menus fit within the screen bounds for mobile …
Browse files Browse the repository at this point in the history
…devices

Fixes fyne-io#1358
  • Loading branch information
andydotxyz committed Oct 12, 2020
1 parent 94afbb9 commit 92c15a5
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 19 deletions.
5 changes: 5 additions & 0 deletions driver/mobile/device.go
@@ -1,8 +1,13 @@
// Package mobile provides mobile specific driver functionality.
package mobile

import "fyne.io/fyne"

// Device describes functionality only available on mobile
type Device interface {
// ScreenInsets returns the space around a mobile screen that should not be drawn used for interactive elements
ScreenInsets() (topLeft, bottomRight fyne.Size)

// Request that the mobile device show the touch screen keyboard (standard layout)
ShowVirtualKeyboard()
// Request that the mobile device show the touch screen keyboard (custom layout)
Expand Down
1 change: 1 addition & 0 deletions internal/driver/glfw/canvas.go
Expand Up @@ -602,6 +602,7 @@ func newCanvas() *glCanvas {
c.overlays = &overlayStack{
OverlayStack: internal.OverlayStack{
OnChange: c.overlayChanged,
Canvas: c,
},
}

Expand Down
19 changes: 5 additions & 14 deletions internal/driver/gomobile/canvas.go
Expand Up @@ -85,25 +85,16 @@ 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
}

offset := fyne.NewPos(0, 0)
devicePadTopLeft, devicePadBottomRight := c.edgePadding()
devicePadTopLeft, devicePadBottomRight := fyne.Size{}, fyne.Size{}
if dev, ok := fyne.CurrentDevice().(mobile.Device); ok { // not present in testing
devicePadTopLeft, devicePadBottomRight = dev.ScreenInsets()
}

if c.windowHead != nil {
topHeight := c.windowHead.MinSize().Height
Expand Down Expand Up @@ -519,7 +510,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
12 changes: 12 additions & 0 deletions internal/driver/gomobile/device.go
Expand Up @@ -20,6 +20,18 @@ var (
// Declare conformity with Device
var _ fyne.Device = (*device)(nil)

func (*device) ScreenInsets() (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 (*device) Orientation() fyne.DeviceOrientation {
switch currentOrientation {
case size.OrientationLandscape:
Expand Down
5 changes: 4 additions & 1 deletion internal/driver/gomobile/menu.go
Expand Up @@ -59,7 +59,10 @@ func (c *mobileCanvas) showMenu(menu *fyne.MainMenu) {
shadow := canvas.NewHorizontalGradient(theme.ShadowColor(), color.Transparent)
c.menu = fyne.NewContainer(panel, shadow)

devicePadTopLeft, devicePadBottomRight := c.edgePadding()
devicePadTopLeft, devicePadBottomRight := fyne.Size{}, fyne.Size{}
if dev, ok := fyne.CurrentDevice().(*device); ok { // not present in testing
devicePadTopLeft, devicePadBottomRight = dev.ScreenInsets()
}
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))
Expand Down
17 changes: 17 additions & 0 deletions internal/overlay_stack.go
Expand Up @@ -4,12 +4,15 @@ import (
"sync"

"fyne.io/fyne"
"fyne.io/fyne/driver/mobile"
"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 +31,20 @@ func (s *OverlayStack) Add(overlay fyne.CanvasObject) {
return
}
s.overlays = append(s.overlays, overlay)

devicePadTopLeft, devicePadBottomRight := fyne.Size{}, fyne.Size{}
if dev, ok := fyne.CurrentDevice().(mobile.Device); ok { // not present in testing
devicePadTopLeft, devicePadBottomRight = dev.ScreenInsets()
}
innerSize := s.Canvas.Size().Subtract(devicePadTopLeft).Subtract(devicePadBottomRight)
topLeft := fyne.NewPos(devicePadTopLeft.Width, devicePadTopLeft.Height)

// TODO this should probably apply to all once #707 is addressed
if _, ok := overlay.(*widget.OverlayContainer); ok {
overlay.Resize(innerSize)
overlay.Move(topLeft)
}

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
5 changes: 3 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
8 changes: 7 additions & 1 deletion widget/menu.go
Expand Up @@ -3,6 +3,7 @@ package widget
import (
"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/driver/mobile"
"fyne.io/fyne/internal/widget"
"fyne.io/fyne/layout"
"fyne.io/fyne/theme"
Expand Down Expand Up @@ -160,7 +161,12 @@ 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 {
bottomEdge := 0
if dev, ok := fyne.CurrentDevice().(mobile.Device); ok {
_, bottomRight := dev.ScreenInsets()
bottomEdge = bottomRight.Height
}
if ah := c.Size().Height - bottomEdge - ap.Y; ah < boxSize.Height {
scrollSize = fyne.NewSize(boxSize.Width, ah)
}
}
Expand Down

0 comments on commit 92c15a5

Please sign in to comment.