Skip to content

Commit 9677021

Browse files
authored
Merge pull request #5514 from andydotxyz/feature/borderside
Provide ability to choose side of window a border button set will be
2 parents f209e8a + ed5ffac commit 9677021

File tree

2 files changed

+126
-15
lines changed

2 files changed

+126
-15
lines changed

container/innerwindow.go

+98-14
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package container
22

33
import (
44
"image/color"
5+
"runtime"
6+
"strings"
57

68
"fyne.io/fyne/v2"
79
"fyne.io/fyne/v2/canvas"
@@ -34,8 +36,16 @@ type InnerWindow struct {
3436
OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func() `json:"-"`
3537
Icon fyne.Resource
3638

37-
title string
38-
content *fyne.Container
39+
// Alignment allows an inner window to specify if the buttons should be on the left
40+
// (`ButtonAlignLeading`) or right of the window border.
41+
//
42+
// Since: 2.6
43+
Alignment widget.ButtonAlign
44+
45+
title string
46+
borderIcon *borderButton
47+
content *fyne.Container
48+
maximized bool
3949
}
4050

4151
// NewInnerWindow creates a new window border around the given `content`, displaying the `title` along the top.
@@ -75,32 +85,43 @@ func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
7585
})
7686
buttons := NewCenter(NewHBox(close, min, max))
7787

78-
var icon fyne.CanvasObject
79-
88+
var iconObj fyne.CanvasObject
89+
var borderIcon *borderButton
8090
if w.Icon != nil {
81-
icon = newBorderButton(w.Icon, modeIcon, th, func() {
91+
borderIcon = newBorderButton(w.Icon, modeIcon, th, func() {
8292
if f := w.OnTappedIcon; f != nil {
8393
f()
8494
}
8595
})
8696
if w.OnTappedIcon == nil {
87-
icon.(*borderButton).Disable()
97+
borderIcon.Disable()
8898
}
99+
iconObj = borderIcon
100+
w.borderIcon = borderIcon
89101
}
90102
title := newDraggableLabel(w.title, w)
91103
title.Truncation = fyne.TextTruncateEllipsis
92104

93105
height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight)
94106
off := (height - title.labelMinSize().Height) / 2
95-
bar := NewBorder(nil, nil, buttons, icon,
96-
New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title))
107+
barMid := New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title)
108+
bar := NewBorder(nil, nil, buttons, iconObj, barMid)
109+
if w.buttonPosition() == widget.ButtonAlignTrailing {
110+
buttons := NewCenter(NewHBox(min, max, close))
111+
bar.Layout = layout.NewBorderLayout(nil, nil, iconObj, buttons)
112+
}
113+
97114
bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
98115
contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v))
99116
corner := newDraggableCorner(w)
100117

118+
if w.content == nil {
119+
w.content = NewPadded(canvas.NewRectangle(color.Transparent))
120+
}
101121
objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner}
102122
return &innerWindowRenderer{ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel),
103-
win: w, bar: bar, buttons: []*borderButton{min, max, close}, bg: bg, corner: corner, contentBG: contentBG}
123+
win: w, bar: bar, buttonBox: buttons, buttons: []*borderButton{close, min, max}, bg: bg,
124+
corner: corner, contentBG: contentBG, icon: borderIcon}
104125
}
105126

106127
func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
@@ -109,6 +130,14 @@ func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
109130
w.content.Refresh()
110131
}
111132

133+
// SetMaximized tells the window if the maximized state should be set or not.
134+
//
135+
// Since: 2.6
136+
func (w *InnerWindow) SetMaximized(max bool) {
137+
w.maximized = max
138+
w.Refresh()
139+
}
140+
112141
func (w *InnerWindow) SetPadded(pad bool) {
113142
if pad {
114143
w.content.Layout = layout.NewPaddedLayout()
@@ -123,16 +152,29 @@ func (w *InnerWindow) SetTitle(title string) {
123152
w.Refresh()
124153
}
125154

155+
func (w *InnerWindow) buttonPosition() widget.ButtonAlign {
156+
if w.Alignment != widget.ButtonAlignCenter {
157+
return w.Alignment
158+
}
159+
160+
if runtime.GOOS == "windows" || runtime.GOOS == "linux" || strings.Contains(runtime.GOOS, "bsd") {
161+
return widget.ButtonAlignTrailing
162+
}
163+
// macOS
164+
return widget.ButtonAlignLeading
165+
}
166+
126167
var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil)
127168

128169
type innerWindowRenderer struct {
129170
*intWidget.ShadowingRenderer
130171

131-
win *InnerWindow
132-
bar *fyne.Container
133-
buttons []*borderButton
134-
bg, contentBG *canvas.Rectangle
135-
corner fyne.CanvasObject
172+
win *InnerWindow
173+
bar, buttonBox *fyne.Container
174+
buttons []*borderButton
175+
icon *borderButton
176+
bg, contentBG *canvas.Rectangle
177+
corner fyne.CanvasObject
136178
}
137179

138180
func (i *innerWindowRenderer) Layout(size fyne.Size) {
@@ -177,14 +219,48 @@ func (i *innerWindowRenderer) Refresh() {
177219
i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v)
178220
i.contentBG.Refresh()
179221

222+
var icon fyne.CanvasObject
223+
if i.icon != nil {
224+
icon = i.icon
225+
}
226+
if i.win.buttonPosition() == widget.ButtonAlignTrailing {
227+
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[1], i.buttons[2], i.buttons[0]}
228+
i.bar.Layout = layout.NewBorderLayout(nil, nil, icon, i.buttonBox)
229+
} else {
230+
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[0], i.buttons[1], i.buttons[2]}
231+
i.bar.Layout = layout.NewBorderLayout(nil, nil, i.buttonBox, icon)
232+
}
180233
for _, b := range i.buttons {
181234
b.setTheme(th)
182235
}
183236
i.bar.Refresh()
184237

238+
if i.win.OnMinimized == nil {
239+
i.buttons[1].Disable()
240+
} else {
241+
i.buttons[1].SetOnTapped(i.win.OnMinimized)
242+
i.buttons[1].Enable()
243+
}
244+
245+
max := i.buttons[2]
246+
if i.win.OnMaximized == nil {
247+
i.buttons[2].Disable()
248+
} else {
249+
max.SetOnTapped(i.win.OnMaximized)
250+
max.Enable()
251+
}
252+
if i.win.maximized {
253+
max.b.SetIcon(theme.ViewRestoreIcon())
254+
} else {
255+
max.b.SetIcon(theme.WindowMaximizeIcon())
256+
}
257+
185258
title := i.bar.Objects[0].(*fyne.Container).Objects[0].(*draggableLabel)
186259
title.SetText(i.win.title)
187260
i.ShadowingRenderer.RefreshShadow()
261+
if i.icon != nil {
262+
i.icon.b.SetIcon(i.win.Icon)
263+
}
188264
}
189265

190266
type draggableLabel struct {
@@ -279,6 +355,14 @@ func (b *borderButton) Disable() {
279355
b.b.Disable()
280356
}
281357

358+
func (b *borderButton) Enable() {
359+
b.b.Enable()
360+
}
361+
362+
func (b *borderButton) SetOnTapped(fn func()) {
363+
b.b.OnTapped = fn
364+
}
365+
282366
func (b *borderButton) MinSize() fyne.Size {
283367
height := b.Theme().Size(theme.SizeNameWindowButtonHeight)
284368
return fyne.NewSquareSize(height)

container/innerwindow_test.go

+28-1
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,31 @@ package container
33
import (
44
"testing"
55

6+
"github.com/stretchr/testify/assert"
7+
68
"fyne.io/fyne/v2"
79
"fyne.io/fyne/v2/internal/cache"
810
"fyne.io/fyne/v2/test"
911
"fyne.io/fyne/v2/theme"
1012
"fyne.io/fyne/v2/widget"
11-
"github.com/stretchr/testify/assert"
1213
)
1314

15+
func TestInnerWindow_Alignment(t *testing.T) {
16+
w := NewInnerWindow("Title", widget.NewLabel("Content"))
17+
w.Resize(fyne.NewSize(150, 100))
18+
assert.Equal(t, widget.ButtonAlignCenter, w.Alignment)
19+
assert.NotEqual(t, widget.ButtonAlignCenter, w.buttonPosition())
20+
21+
buttons := test.WidgetRenderer(w).(*innerWindowRenderer).buttonBox
22+
w.Alignment = widget.ButtonAlignLeading
23+
w.Refresh()
24+
assert.Zero(t, buttons.Position().X)
25+
26+
w.Alignment = widget.ButtonAlignTrailing
27+
w.Refresh()
28+
assert.Greater(t, buttons.Position().X, float32(100))
29+
}
30+
1431
func TestInnerWindow_Close(t *testing.T) {
1532
w := NewInnerWindow("Thing", widget.NewLabel("Content"))
1633

@@ -60,6 +77,16 @@ func TestInnerWindow_SetContent(t *testing.T) {
6077
assert.Equal(t, "Content2", title.Objects[0].(*widget.Label).Text)
6178
}
6279

80+
func TestInnerWindow_SetMaximized(t *testing.T) {
81+
w := NewInnerWindow("Title", widget.NewLabel("Content"))
82+
83+
icon := test.WidgetRenderer(w).(*innerWindowRenderer).buttons[2]
84+
assert.Equal(t, "foreground_maximize.svg", icon.b.Icon.Name())
85+
86+
w.SetMaximized(true)
87+
assert.Equal(t, "foreground_view-zoom-fit.svg", icon.b.Icon.Name())
88+
}
89+
6390
func TestInnerWindow_SetPadded(t *testing.T) {
6491
w := NewInnerWindow("Title", widget.NewLabel("Content"))
6592
minPadded := w.MinSize()

0 commit comments

Comments
 (0)