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

Support clipping child clips, avoid overflow of nested scroll containers #1695

Merged
merged 4 commits into from Dec 29, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -14,6 +14,7 @@ More detailed release notes can be found on the [releases page](https://github.c
* Fix possible race conditions for canvas capture
* Improvements to `fyne get` command downloader
* Fix tree, so it refreshes visible nodes on Refresh()
* Incorrect clipping behaviour with nested scroll containers (#1682)


## 1.4.2 - 9 December 2020
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -7,7 +7,7 @@ require (
github.com/akavel/rsrc v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/fyne-io/mobile v0.1.2-0.20201211141148-290e90fd7a4f
github.com/fyne-io/mobile v0.1.2
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3
github.com/godbus/dbus/v5 v5.0.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Expand Up @@ -10,8 +10,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fyne-io/glfw/v3.3/glfw v0.0.0-20201123143003-f2279069162d h1:WfVxpuVm+5Gr3ipAoWrxV8lJFYkaBWoEwFRrWThWRSU=
github.com/fyne-io/glfw/v3.3/glfw v0.0.0-20201123143003-f2279069162d/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/fyne-io/mobile v0.1.2-0.20201211141148-290e90fd7a4f h1:bpfV54yP8w21/AjChjuwYwRSsVt28EdG6ase0XJ1epc=
github.com/fyne-io/mobile v0.1.2-0.20201211141148-290e90fd7a4f/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/mobile v0.1.2 h1:0HaXDtOOwyOTn3Umi0uKVCOgJtfX73c6unC4U8i5VZU=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
Expand Down
81 changes: 81 additions & 0 deletions internal/clip.go
@@ -0,0 +1,81 @@
package internal

import "fyne.io/fyne"

// ClipStack keeps track of the areas that should be clipped when drawing a canvas.
// If no clips are present then adding one will be added as-is.
// Subsequent items pushed will be completely within the previous clip.
type ClipStack struct {
clips []*ClipItem
}

// Pop removes the current top clip and returns it.
func (c *ClipStack) Pop() *ClipItem {
if len(c.clips) == 0 {
return nil
}

ret := c.clips[len(c.clips)-1]
c.clips = c.clips[:len(c.clips)-1]
return ret
}

// Length returns the number of items in this clip stack. 0 means no clip.
func (c *ClipStack) Length() int {
return len(c.clips)
}

// Push a new clip onto this stack at position and size specified.
// The returned clip item is the result of calculating the intersection of the requested clip and it's parent.
func (c *ClipStack) Push(p fyne.Position, s fyne.Size) *ClipItem {
outer := c.Top()
inner := outer.Intersect(p, s)

c.clips = append(c.clips, inner)
return inner
}

// Top returns the current clip item - it will always be within the bounds of any parent clips.
func (c *ClipStack) Top() *ClipItem {
if len(c.clips) == 0 {
return nil
}

return c.clips[len(c.clips)-1]
}

// ClipItem represents a single clip in a clip stack, denoted by a size and position.
type ClipItem struct {
pos fyne.Position
size fyne.Size
}

// Rect returns the position and size parameters of the clip.
func (i *ClipItem) Rect() (fyne.Position, fyne.Size) {
return i.pos, i.size
}

// Intersect returns a new clip item that is the intersection of the requested parameters and this clip.
func (i *ClipItem) Intersect(p fyne.Position, s fyne.Size) *ClipItem {
ret := &ClipItem{p, s}
if i == nil {
return ret
}

if ret.pos.X < i.pos.X {
ret.pos.X = i.pos.X
ret.size.Width -= i.pos.X - p.X
}
if ret.pos.Y < i.pos.Y {
ret.pos.Y = i.pos.Y
ret.size.Height -= i.pos.Y - p.Y
}

if p.X+s.Width > i.pos.X+i.size.Width {
ret.size.Width = (i.pos.X + i.size.Width) - ret.pos.X
}
if p.Y+s.Height > i.pos.Y+i.size.Height {
ret.size.Height = (i.pos.Y + i.size.Height) - ret.pos.Y
}
return ret
}
62 changes: 62 additions & 0 deletions internal/clip_test.go
@@ -0,0 +1,62 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"

"fyne.io/fyne"
)

func TestClipStack_Intersect(t *testing.T) {
p1 := fyne.NewPos(5, 25)
s1 := fyne.NewSize(100, 100)
c := &ClipStack{
clips: []*ClipItem{
{p1, s1},
},
}

p2 := fyne.NewPos(25, 0)
s2 := fyne.NewSize(50, 50)
i := c.Push(p2, s2)

assert.Equal(t, fyne.NewPos(25, 25), i.pos)
assert.Equal(t, fyne.NewSize(50, 25), i.size)
assert.Equal(t, 2, len(c.clips))

_ = c.Pop()
p2 = fyne.NewPos(50, 50)
s2 = fyne.NewSize(150, 50)
i = c.Push(p2, s2)

assert.Equal(t, fyne.NewPos(50, 50), i.pos)
assert.Equal(t, fyne.NewSize(55, 50), i.size)
assert.Equal(t, 2, len(c.clips))
}

func TestClipStack_Pop(t *testing.T) {
p := fyne.NewPos(5, 5)
s := fyne.NewSize(100, 100)
c := &ClipStack{
clips: []*ClipItem{
{p, s},
},
}

i := c.Pop()
assert.Equal(t, p, i.pos)
assert.Equal(t, s, i.size)
assert.Equal(t, 0, len(c.clips))
}

func TestClipStack_Push(t *testing.T) {
c := &ClipStack{}
p := fyne.NewPos(5, 5)
s := fyne.NewSize(100, 100)

i := c.Push(p, s)
assert.Equal(t, p, i.pos)
assert.Equal(t, s, i.size)
assert.Equal(t, 1, len(c.clips))
}
20 changes: 12 additions & 8 deletions internal/driver/glfw/canvas.go
Expand Up @@ -405,6 +405,7 @@ func (c *glCanvas) overlayChanged() {
}

func (c *glCanvas) paint(size fyne.Size) {
clips := &internal.ClipStack{}
if c.Content() == nil {
return
}
Expand All @@ -413,18 +414,21 @@ func (c *glCanvas) paint(size fyne.Size) {

paint := func(node *renderCacheNode, pos fyne.Position) {
obj := node.obj
// TODO should this be somehow not scroll container specific?
if _, ok := obj.(*widget.ScrollContainer); ok {
c.painter.StartClipping(
fyne.NewPos(pos.X, c.Size().Height-pos.Y-obj.Size().Height),
obj.Size(),
)
if _, ok := obj.(fyne.Scrollable); ok {
inner := clips.Push(pos, obj.Size())
c.painter.StartClipping(inner.Rect())
}
c.painter.Paint(obj, pos, size)
}
afterPaint := func(node *renderCacheNode) {
if _, ok := node.obj.(*widget.ScrollContainer); ok {
c.painter.StopClipping()
if _, ok := node.obj.(fyne.Scrollable); ok {
clips.Pop()
if top := clips.Top(); top != nil {
c.painter.StartClipping(top.Rect())
} else {
c.painter.StopClipping()

}
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/driver/gomobile/canvas.go
Expand Up @@ -68,7 +68,7 @@ func (c *mobileCanvas) InteractiveArea() (fyne.Position, fyne.Size) {
func (c *mobileCanvas) SetContent(content fyne.CanvasObject) {
c.content = content

c.sizeContent(c.Size().Max(content.MinSize()))
c.sizeContent(c.Size()) // fixed window size for mobile, cannot stretch to new content
}

// Deprecated: Use Overlays() instead.
Expand Down
1 change: 1 addition & 0 deletions internal/driver/gomobile/canvas_test.go
Expand Up @@ -251,6 +251,7 @@ func TestCanvas_Focusable(t *testing.T) {
content := newFocusableEntry()
c := NewCanvas().(*mobileCanvas)
c.SetContent(content)
c.resize(fyne.NewSize(25, 25))

c.tapDown(fyne.NewPos(10, 10), 0)
assert.Equal(t, 1, content.focusedTimes)
Expand Down
33 changes: 17 additions & 16 deletions internal/driver/gomobile/driver.go
Expand Up @@ -5,22 +5,21 @@ import (
"strconv"
"time"

"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/internal"
"fyne.io/fyne/internal/driver"
"fyne.io/fyne/internal/painter"
pgl "fyne.io/fyne/internal/painter/gl"
"fyne.io/fyne/theme"
"fyne.io/fyne/widget"

"github.com/fyne-io/mobile/app"
"github.com/fyne-io/mobile/event/key"
"github.com/fyne-io/mobile/event/lifecycle"
"github.com/fyne-io/mobile/event/paint"
"github.com/fyne-io/mobile/event/size"
"github.com/fyne-io/mobile/event/touch"
"github.com/fyne-io/mobile/gl"

"fyne.io/fyne"
"fyne.io/fyne/canvas"
"fyne.io/fyne/internal"
"fyne.io/fyne/internal/driver"
"fyne.io/fyne/internal/painter"
pgl "fyne.io/fyne/internal/painter/gl"
"fyne.io/fyne/theme"
)

const tapSecondaryDelay = 300 * time.Millisecond
Expand Down Expand Up @@ -186,6 +185,7 @@ func (d *mobileDriver) onStop() {
}

func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) {
clips := &internal.ClipStack{}
canvas := window.Canvas().(*mobileCanvas)

r, g, b, a := theme.BackgroundColor().RGBA()
Expand All @@ -194,19 +194,20 @@ func (d *mobileDriver) paintWindow(window fyne.Window, size fyne.Size) {
d.glctx.Clear(gl.COLOR_BUFFER_BIT)

paint := func(obj fyne.CanvasObject, pos fyne.Position, _ fyne.Position, _ fyne.Size) bool {
// TODO should this be somehow not scroll container specific?
if _, ok := obj.(*widget.ScrollContainer); ok {
canvas.painter.StartClipping(
fyne.NewPos(pos.X, canvas.Size().Height-pos.Y-obj.Size().Height),
obj.Size(),
)
if _, ok := obj.(fyne.Scrollable); ok {
inner := clips.Push(pos, obj.Size())
canvas.painter.StartClipping(inner.Rect())
}
canvas.painter.Paint(obj, pos, size)
return false
}
afterPaint := func(obj, _ fyne.CanvasObject) {
if _, ok := obj.(*widget.ScrollContainer); ok {
if _, ok := obj.(fyne.Scrollable); ok {
canvas.painter.StopClipping()
clips.Pop()
if top := clips.Top(); top != nil {
canvas.painter.StartClipping(top.Rect())
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/painter/gl/painter.go
Expand Up @@ -53,7 +53,7 @@ func (p *glPainter) Clear() {

func (p *glPainter) StartClipping(pos fyne.Position, size fyne.Size) {
x := p.textureScaleInt(pos.X)
y := p.textureScaleInt(pos.Y)
y := p.textureScaleInt(p.canvas.Size().Height - pos.Y - size.Height)
w := p.textureScaleInt(size.Width)
h := p.textureScaleInt(size.Height)
p.glScissorOpen(int32(x), int32(y), int32(w), int32(h))
Expand Down
15 changes: 7 additions & 8 deletions vendor/github.com/fyne-io/mobile/app/darwin_ios.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions vendor/github.com/fyne-io/mobile/app/darwin_ios.m

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/modules.txt
Expand Up @@ -8,7 +8,7 @@ github.com/akavel/rsrc/ico
github.com/davecgh/go-spew/spew
# github.com/fsnotify/fsnotify v1.4.9
github.com/fsnotify/fsnotify
# github.com/fyne-io/mobile v0.1.2-0.20201211141148-290e90fd7a4f
# github.com/fyne-io/mobile v0.1.2
github.com/fyne-io/mobile/app
github.com/fyne-io/mobile/app/internal/callfn
github.com/fyne-io/mobile/event/key
Expand Down