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

Adding inner window and multi window containers #4245

Merged
merged 7 commits into from
Sep 25, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 6 additions & 1 deletion 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 Down Expand Up @@ -186,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"},
"containers": {"apptabs", "border", "box", "center", "doctabs", "grid", "scroll", "split", "innerwindow"},
"widgets": {"accordion", "activity", "button", "card", "entry", "form", "input", "progress", "text", "toolbar"},
}
)
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
Loading