Skip to content

Commit

Permalink
Improving inner window theme (#5468)
Browse files Browse the repository at this point in the history
* Move sizes to size.go

* Update look of the inner window

And make it more themeable too

* Fixing window position / size maths

* Avoid defining new button colours at this time
  • Loading branch information
andydotxyz authored Jan 26, 2025
1 parent e4211e5 commit 04d17a5
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 71 deletions.
7 changes: 7 additions & 0 deletions cmd/fyne_demo/tutorials/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tutorials
import (
"fmt"
"image/color"
"log"
"strconv"

"fyne.io/fyne/v2"
Expand Down Expand Up @@ -116,6 +117,12 @@ func makeInnerWindowTab(_ fyne.Window) fyne.CanvasObject {
label.SetText("Tapped")
})))
win1.Icon = data.FyneLogo
win1.OnMaximized = func() {
log.Println("Should maximize here")
}
win1.OnMinimized = func() {
log.Println("Should minimize here")
}

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

Expand Down
156 changes: 124 additions & 32 deletions container/innerwindow.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package container

import (
"image/color"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
intWidget "fyne.io/fyne/v2/internal/widget"
Expand All @@ -9,6 +11,15 @@ import (
"fyne.io/fyne/v2/widget"
)

type titleBarButtonMode int

const (
modeClose titleBarButtonMode = iota
modeMinimize
modeMaximize
modeIcon
)

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

// InnerWindow defines a container that wraps content in a window border - that can then be placed inside
Expand Down Expand Up @@ -43,50 +54,53 @@ func (w *InnerWindow) Close() {

func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
w.ExtendBaseWidget(w)
th := w.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()

min := &widget.Button{Icon: theme.WindowMinimizeIcon(), Importance: widget.LowImportance, OnTapped: w.OnMinimized}
min := newBorderButton(theme.WindowMinimizeIcon(), modeMinimize, th, w.OnMinimized)
if w.OnMinimized == nil {
min.Disable()
}
max := &widget.Button{Icon: theme.WindowMaximizeIcon(), Importance: widget.LowImportance, OnTapped: w.OnMaximized}
max := newBorderButton(theme.WindowMaximizeIcon(), modeMaximize, th, 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)
close := newBorderButton(theme.WindowCloseIcon(), modeClose, th, func() {
if f := w.CloseIntercept; f != nil {
f()
} else {
w.Close()
}
})
buttons := NewCenter(NewHBox(close, min, max))

var icon fyne.CanvasObject

if w.Icon != nil {
icon = &widget.Button{Icon: w.Icon, Importance: widget.LowImportance, OnTapped: func() {
icon = newBorderButton(w.Icon, modeIcon, th, func() {
if f := w.OnTappedIcon; f != nil {
f()
}
}}
})
if w.OnTappedIcon == nil {
icon.(*widget.Button).Disable()
icon.(*borderButton).Disable()
}
}
title := newDraggableLabel(w.title, w)
title.Truncation = fyne.TextTruncateEllipsis
th := w.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()

bar := NewBorder(nil, nil, buttons, icon, title)
height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight)
off := (height - title.labelMinSize().Height) / 2
bar := NewBorder(nil, nil, buttons, icon,
New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title))
bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v))
corner := newDraggableCorner(w)

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

func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
Expand Down Expand Up @@ -116,6 +130,7 @@ type innerWindowRenderer struct {

win *InnerWindow
bar *fyne.Container
buttons []*borderButton
bg, contentBG *canvas.Rectangle
corner fyne.CanvasObject
}
Expand All @@ -124,38 +139,34 @@ func (i *innerWindowRenderer) Layout(size fyne.Size) {
th := i.win.Theme()
pad := th.Size(theme.SizeNamePadding)

pos := fyne.NewSquareOffsetPos(pad / 2)
size = size.Subtract(fyne.NewSquareSize(pad))
i.LayoutShadow(size, pos)

i.bg.Move(pos)
i.LayoutShadow(size, fyne.Position{})
i.bg.Resize(size)

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

innerPos := pos.AddXY(pad, barHeight)
innerPos := fyne.NewPos(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).AddXY(1, 1))
i.corner.Move(fyne.NewPos(size.Components()).Subtract(cornerSize).AddXY(1, 1))
i.corner.Resize(cornerSize)
}

func (i *innerWindowRenderer) MinSize() fyne.Size {
th := i.win.Theme()
pad := th.Size(theme.SizeNamePadding)
contentMin := i.win.content.MinSize()
barMin := i.bar.MinSize()
barHeight := th.Size(theme.SizeNameWindowTitleBarHeight)

innerWidth := fyne.Max(barMin.Width, contentMin.Width)
innerWidth := fyne.Max(i.bar.MinSize().Width, contentMin.Width)

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

func (i *innerWindowRenderer) Refresh() {
Expand All @@ -165,9 +176,13 @@ func (i *innerWindowRenderer) Refresh() {
i.bg.Refresh()
i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v)
i.contentBG.Refresh()

for _, b := range i.buttons {
b.setTheme(th)
}
i.bar.Refresh()

title := i.bar.Objects[0].(*draggableLabel)
title := i.bar.Objects[0].(*fyne.Container).Objects[0].(*draggableLabel)
title.SetText(i.win.title)
i.ShadowingRenderer.RefreshShadow()
}
Expand All @@ -193,12 +208,22 @@ func (d *draggableLabel) Dragged(ev *fyne.DragEvent) {
func (d *draggableLabel) DragEnd() {
}

func (d *draggableLabel) Tapped(ev *fyne.PointEvent) {
func (d *draggableLabel) MinSize() fyne.Size {
width := d.Label.MinSize().Width
height := d.Label.Theme().Size(theme.SizeNameWindowButtonHeight)
return fyne.NewSize(width, height)
}

func (d *draggableLabel) Tapped(_ *fyne.PointEvent) {
if f := d.win.OnTappedBar; f != nil {
f()
}
}

func (d *draggableLabel) labelMinSize() fyne.Size {
return d.Label.MinSize()
}

type draggableCorner struct {
widget.BaseWidget
win *InnerWindow
Expand All @@ -224,3 +249,70 @@ func (c *draggableCorner) Dragged(ev *fyne.DragEvent) {

func (c *draggableCorner) DragEnd() {
}

type borderButton struct {
widget.BaseWidget

b *widget.Button
c *ThemeOverride
mode titleBarButtonMode
}

func newBorderButton(icon fyne.Resource, mode titleBarButtonMode, th fyne.Theme, fn func()) *borderButton {
buttonImportance := widget.MediumImportance
if mode == modeIcon {
buttonImportance = widget.LowImportance
}
b := &widget.Button{Icon: icon, Importance: buttonImportance, OnTapped: fn}
c := NewThemeOverride(b, &buttonTheme{Theme: th, mode: mode})

ret := &borderButton{b: b, c: c, mode: mode}
ret.ExtendBaseWidget(ret)
return ret
}

func (b *borderButton) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(b.c)
}

func (b *borderButton) Disable() {
b.b.Disable()
}

func (b *borderButton) MinSize() fyne.Size {
height := b.Theme().Size(theme.SizeNameWindowButtonHeight)
return fyne.NewSquareSize(height)
}

func (b *borderButton) setTheme(th fyne.Theme) {
b.c.Theme = &buttonTheme{Theme: th, mode: b.mode}
}

type buttonTheme struct {
fyne.Theme
mode titleBarButtonMode
}

func (b *buttonTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
switch n {
case theme.ColorNameHover:
if b.mode == modeClose {
n = theme.ColorNameError
}
}
return b.Theme.Color(n, v)
}

func (b *buttonTheme) Size(n fyne.ThemeSizeName) float32 {
switch n {
case theme.SizeNameInputRadius:
if b.mode == modeIcon {
return 0
}
n = theme.SizeNameWindowButtonRadius
case theme.SizeNameInlineIcon:
n = theme.SizeNameWindowButtonIcon
}

return b.Theme.Size(n)
}
2 changes: 1 addition & 1 deletion container/innerwindow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestInnerWindow_SetPadded(t *testing.T) {
func TestInnerWindow_SetTitle(t *testing.T) {
w := NewInnerWindow("Title1", widget.NewLabel("Content"))
r := cache.Renderer(w).(*innerWindowRenderer)
title := r.bar.Objects[0].(*draggableLabel)
title := r.bar.Objects[0].(*fyne.Container).Objects[0].(*draggableLabel)
assert.Equal(t, "Title1", title.Text)

w.SetTitle("Title2")
Expand Down
68 changes: 67 additions & 1 deletion theme/size.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,27 @@ const (
// SizeNameScrollBarRadius is the name of theme lookup for the scroll bar corner radius.
//
// Since: 2.5
SizeNameScrollBarRadius = "scrollBarRadius"
SizeNameScrollBarRadius fyne.ThemeSizeName = "scrollBarRadius"

// SizeNameWindowButtonHeight is the name of the height for an inner window titleBar button.
//
// Since: 2.6
SizeNameWindowButtonHeight fyne.ThemeSizeName = "windowButtonHeight"

// SizeNameWindowButtonRadius is the name of the radius for an inner window titleBar button.
//
// Since: 2.6
SizeNameWindowButtonRadius fyne.ThemeSizeName = "windowButtonRadius"

// SizeNameWindowButtonIcon is the name of the width of an inner window titleBar button.
//
// Since: 2.6
SizeNameWindowButtonIcon fyne.ThemeSizeName = "windowButtonIcon"

// SizeNameWindowTitleBarHeight is the height for inner window titleBars.
//
// Since: 2.6
SizeNameWindowTitleBarHeight fyne.ThemeSizeName = "windowTitleBarHeight"
)

// CaptionTextSize returns the size for caption text.
Expand Down Expand Up @@ -179,3 +199,49 @@ func TextSize() float32 {
func TextSubHeadingSize() float32 {
return Current().Size(SizeNameSubHeadingText)
}

func (t *builtinTheme) Size(s fyne.ThemeSizeName) float32 {
switch s {
case SizeNameSeparatorThickness:
return 1
case SizeNameInlineIcon:
return 20
case SizeNameInnerPadding:
return 8
case SizeNameLineSpacing:
return 4
case SizeNamePadding:
return 4
case SizeNameScrollBar:
return 12
case SizeNameScrollBarSmall:
return 3
case SizeNameText:
return 14
case SizeNameHeadingText:
return 24
case SizeNameSubHeadingText:
return 18
case SizeNameCaptionText:
return 11
case SizeNameInputBorder:
return 1
case SizeNameInputRadius:
return 5
case SizeNameSelectionRadius:
return 3
case SizeNameScrollBarRadius:
return 3
case SizeNameWindowButtonHeight:
return 16
case SizeNameWindowButtonRadius:
return 8
case SizeNameWindowButtonIcon:
return 14
case SizeNameWindowTitleBarHeight:
return 26

default:
return 0
}
}
Loading

0 comments on commit 04d17a5

Please sign in to comment.