Skip to content

Commit

Permalink
Merge branch 'develop' into fix/2165
Browse files Browse the repository at this point in the history
  • Loading branch information
andydotxyz authored Sep 27, 2023
2 parents f5e6821 + d35e57e commit 2dcde5f
Show file tree
Hide file tree
Showing 26 changed files with 1,009 additions and 95 deletions.
17 changes: 17 additions & 0 deletions cmd/fyne_demo/tutorials/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/cmd/fyne_demo/data"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)

Expand Down Expand Up @@ -107,6 +108,22 @@ func makeGridLayout(_ fyne.Window) fyne.CanvasObject {
box1, box2, box3, box4)
}

func makeInnerWindowTab(_ fyne.Window) fyne.CanvasObject {
label := widget.NewLabel("Window content for inner demo")
win1 := container.NewInnerWindow("Inner Demo", container.NewVBox(
label,
widget.NewButton("Tap Me", func() {
label.SetText("Tapped")
})))
win1.Icon = theme.FyneLogo()

win2 := container.NewInnerWindow("Inner2", widget.NewLabel("Win 2"))

multi := container.NewMultipleWindows()
multi.Windows = []*container.InnerWindow{win1, win2}
return multi
}

func makeScrollTab(_ fyne.Window) fyne.CanvasObject {
hlist := makeButtonList(20)
vlist := makeButtonList(50)
Expand Down
14 changes: 12 additions & 2 deletions cmd/fyne_demo/tutorials/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ var (
makeScrollTab,
true,
},
"innerwindow": {"InnerWindow",
"A window that can be used inside a traditional window to contain a document or content.",
makeInnerWindowTab,
true,
},
"widgets": {"Widgets",
"In this section you can see the features available in the toolkit widget set.\n" +
"Expand the tree on the left to browse the individual tutorial elements.",
Expand All @@ -88,6 +93,11 @@ var (
makeAccordionTab,
true,
},
"activity": {"Activity",
"A spinner indicating activity used in buttons etc.",
makeActivityTab,
true,
},
"button": {"Button",
"Simple widget for user tap handling.",
makeButtonTab,
Expand Down Expand Up @@ -181,7 +191,7 @@ var (
TutorialIndex = map[string][]string{
"": {"welcome", "canvas", "animations", "icons", "widgets", "collections", "containers", "dialogs", "windows", "binding", "advanced"},
"collections": {"list", "table", "tree", "gridwrap"},
"containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split"},
"widgets": {"accordion", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
"containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split", "innerwindow"},
"widgets": {"accordion", "activity", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
}
)
50 changes: 50 additions & 0 deletions cmd/fyne_demo/tutorials/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fyne.io/fyne/v2/cmd/fyne_demo/data"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/validation"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/mobile"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
Expand Down Expand Up @@ -55,6 +56,55 @@ func makeAccordionTab(_ fyne.Window) fyne.CanvasObject {
return ac
}

func makeActivityTab(win fyne.Window) fyne.CanvasObject {
a1 := widget.NewActivity()
a2 := widget.NewActivity()

var button *widget.Button
start := func() {
button.Disable()
a1.Start()
a1.Show()
a2.Start()
a2.Show()

defer func() {
go func() {
time.Sleep(time.Second * 10)
a1.Stop()
a1.Hide()
a2.Stop()
a2.Hide()

button.Enable()
}()
}()
}

button = widget.NewButton("Animate", start)
start()

return container.NewCenter(container.NewGridWithColumns(1,
container.NewCenter(container.NewVBox(
container.NewHBox(widget.NewLabel("Working..."), a1),
container.NewStack(button, a2))),
container.NewCenter(widget.NewButton("Show dialog", func() {
prop := canvas.NewRectangle(color.Transparent)
prop.SetMinSize(fyne.NewSize(50, 50))

a3 := widget.NewActivity()
d := dialog.NewCustomWithoutButtons("Please wait...", container.NewStack(prop, a3), win)
a3.Start()
d.Show()

go func() {
time.Sleep(time.Second * 5)
a3.Stop()
d.Hide()
}()
}))))
}

func makeButtonTab(_ fyne.Window) fyne.CanvasObject {
disabled := widget.NewButton("Disabled", func() {})
disabled.Disable()
Expand Down
203 changes: 203 additions & 0 deletions container/innerwindow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package container

import (
"image/color"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
intWidget "fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)

var _ fyne.Widget = (*InnerWindow)(nil)

// InnerWindow defines a container that wraps content in a window border - that can then be placed inside
// a regular container/canvas.
//
// Since: 2.5
type InnerWindow struct {
widget.BaseWidget

CloseIntercept func()
OnDragged, OnResized func(*fyne.DragEvent)
OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func()
Icon fyne.Resource

title string
content fyne.CanvasObject
}

// NewInnerWindow creates a new window border around the given `content`, displaying the `title` along the top.
// This will behave like a normal contain and will probably want to be added to a `MultipleWindows` parent.
//
// Since: 2.5
func NewInnerWindow(title string, content fyne.CanvasObject) *InnerWindow {
w := &InnerWindow{title: title, content: content}
w.ExtendBaseWidget(w)
return w
}

func (w *InnerWindow) Close() {
w.Hide()
}

func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
w.ExtendBaseWidget(w)

min := &widget.Button{Icon: theme.WindowMinimizeIcon(), Importance: widget.LowImportance, OnTapped: w.OnMinimized}
if w.OnMinimized == nil {
min.Disable()
}
max := &widget.Button{Icon: theme.WindowMaximizeIcon(), Importance: widget.LowImportance, OnTapped: w.OnMaximized}
if w.OnMaximized == nil {
max.Disable()
}

buttons := NewHBox(
&widget.Button{Icon: theme.WindowCloseIcon(), Importance: widget.DangerImportance, OnTapped: func() {
if f := w.CloseIntercept; f != nil {
f()
} else {
w.Close()
}
}},
min, max)

var icon fyne.CanvasObject
if w.Icon != nil {
icon = &widget.Button{Icon: w.Icon, Importance: widget.LowImportance, OnTapped: func() {
if f := w.OnTappedIcon; f != nil {
f()
}
}}
if w.OnTappedIcon == nil {
icon.(*widget.Button).Disable()
}
}
title := newDraggableLabel(w.title, w.OnDragged, w.OnTappedBar)
title.Truncation = fyne.TextTruncateEllipsis

bar := NewBorder(nil, nil, buttons, icon, title)
bg := canvas.NewRectangle(theme.OverlayBackgroundColor())
contentBG := canvas.NewRectangle(theme.BackgroundColor())
corner := newDraggableCorner(w.OnResized)

objects := []fyne.CanvasObject{bg, contentBG, bar, corner, w.content}
return &innerWindowRenderer{ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel),
win: w, bar: bar, bg: bg, corner: corner, contentBG: contentBG}
}

var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil)

type innerWindowRenderer struct {
*intWidget.ShadowingRenderer
min fyne.Size

win *InnerWindow
bar *fyne.Container
bg, contentBG *canvas.Rectangle
corner fyne.CanvasObject
}

func (i *innerWindowRenderer) Layout(size fyne.Size) {
pad := theme.Padding()
pos := fyne.NewSquareOffsetPos(pad / 2)
size = size.Subtract(fyne.NewSquareSize(pad))
i.LayoutShadow(size, pos)

i.bg.Move(pos)
i.bg.Resize(size)

barHeight := i.bar.MinSize().Height
i.bar.Move(pos.AddXY(pad, 0))
i.bar.Resize(fyne.NewSize(size.Width-pad*2, barHeight))

innerPos := pos.AddXY(pad, barHeight)
innerSize := fyne.NewSize(size.Width-pad*2, size.Height-pad-barHeight)
i.contentBG.Move(innerPos)
i.contentBG.Resize(innerSize)
i.win.content.Move(innerPos)
i.win.content.Resize(innerSize)

cornerSize := i.corner.MinSize()
i.corner.Move(pos.Add(size).Subtract(cornerSize))
i.corner.Resize(cornerSize)
}

func (i *innerWindowRenderer) MinSize() fyne.Size {
pad := theme.Padding()
contentMin := i.win.content.MinSize()
barMin := i.bar.MinSize()

// only allow windows to grow, as per normal windows
contentMin = contentMin.Max(i.min)
i.min = contentMin
innerWidth := fyne.Max(barMin.Width, contentMin.Width)

return fyne.NewSize(innerWidth+pad*2, contentMin.Height+pad+barMin.Height).Add(fyne.NewSquareSize(pad))
}

func (i *innerWindowRenderer) Refresh() {
i.bg.FillColor = theme.OverlayBackgroundColor()
i.bg.Refresh()
i.contentBG.FillColor = theme.BackgroundColor()
i.contentBG.Refresh()
i.bar.Refresh()

i.ShadowingRenderer.RefreshShadow()
}

type draggableLabel struct {
widget.Label
drag func(*fyne.DragEvent)
tap func()
}

func newDraggableLabel(title string, fn func(*fyne.DragEvent), tap func()) *draggableLabel {
d := &draggableLabel{drag: fn, tap: tap}
d.ExtendBaseWidget(d)
d.Text = title
return d
}

func (d *draggableLabel) Dragged(ev *fyne.DragEvent) {
if f := d.drag; f != nil {
d.drag(ev)
}
}

func (d *draggableLabel) DragEnd() {
}

func (d *draggableLabel) Tapped(ev *fyne.PointEvent) {
if f := d.tap; f != nil {
d.tap()
}
}

type draggableCorner struct {
widget.BaseWidget
drag func(*fyne.DragEvent)
}

func newDraggableCorner(fn func(*fyne.DragEvent)) *draggableCorner {
d := &draggableCorner{drag: fn}
d.ExtendBaseWidget(d)
return d
}

func (c *draggableCorner) CreateRenderer() fyne.WidgetRenderer {
prop := canvas.NewRectangle(color.Transparent)
prop.SetMinSize(fyne.NewSquareSize(20))
return widget.NewSimpleRenderer(prop)
}

func (c *draggableCorner) Dragged(ev *fyne.DragEvent) {
if f := c.drag; f != nil {
c.drag(ev)
}
}

func (c *draggableCorner) DragEnd() {
}
50 changes: 50 additions & 0 deletions container/innerwindow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package container

import (
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/test"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/stretchr/testify/assert"
)

func TestInnerWindow_Close(t *testing.T) {
w := NewInnerWindow("Thing", widget.NewLabel("Content"))

outer := test.NewWindow(w)
outer.SetPadded(false)
outer.Resize(w.MinSize())
assert.True(t, w.Visible())

closePos := fyne.NewPos(10, 10)
test.TapCanvas(outer.Canvas(), closePos)
assert.False(t, w.Visible())

w.Show()
assert.True(t, w.Visible())

closing := true
w.CloseIntercept = func() {
closing = true
}

test.TapCanvas(outer.Canvas(), closePos)
assert.True(t, closing)
assert.True(t, w.Visible())
}

func TestInnerWindow_MinSize(t *testing.T) {
w := NewInnerWindow("Thing", widget.NewLabel("Content"))

btnMin := widget.NewButtonWithIcon("", theme.WindowCloseIcon(), func() {}).MinSize()
labelMin := widget.NewLabel("Inner").MinSize()

winMin := w.MinSize()
assert.Equal(t, btnMin.Height+labelMin.Height+theme.Padding()*2, winMin.Height)
assert.Greater(t, winMin.Width, btnMin.Width*3+theme.Padding()*3)

w2 := NewInnerWindow("Much longer title that will truncate", widget.NewLabel("Content"))
assert.Equal(t, winMin, w2.MinSize())
}
Loading

0 comments on commit 2dcde5f

Please sign in to comment.