From 04d17a53b2711f2ffaeee0f9b776d2a6d9e4c784 Mon Sep 17 00:00:00 2001 From: Andy Williams Date: Sun, 26 Jan 2025 21:10:44 +0000 Subject: [PATCH] Improving inner window theme (#5468) * 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 --- cmd/fyne_demo/tutorials/container.go | 7 ++ container/innerwindow.go | 156 +++++++++++++++++++++------ container/innerwindow_test.go | 2 +- theme/size.go | 68 +++++++++++- theme/theme.go | 37 ------- 5 files changed, 199 insertions(+), 71 deletions(-) diff --git a/cmd/fyne_demo/tutorials/container.go b/cmd/fyne_demo/tutorials/container.go index 8291c57d83..2ab5bc0028 100644 --- a/cmd/fyne_demo/tutorials/container.go +++ b/cmd/fyne_demo/tutorials/container.go @@ -3,6 +3,7 @@ package tutorials import ( "fmt" "image/color" + "log" "strconv" "fyne.io/fyne/v2" @@ -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")) diff --git a/container/innerwindow.go b/container/innerwindow.go index 32e0839113..290e887ab0 100644 --- a/container/innerwindow.go +++ b/container/innerwindow.go @@ -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" @@ -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 @@ -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) { @@ -116,6 +130,7 @@ type innerWindowRenderer struct { win *InnerWindow bar *fyne.Container + buttons []*borderButton bg, contentBG *canvas.Rectangle corner fyne.CanvasObject } @@ -124,18 +139,14 @@ 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) @@ -143,7 +154,7 @@ func (i *innerWindowRenderer) Layout(size fyne.Size) { 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) } @@ -151,11 +162,11 @@ 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() { @@ -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() } @@ -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 @@ -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) +} diff --git a/container/innerwindow_test.go b/container/innerwindow_test.go index 55d1436ded..8a6825976a 100644 --- a/container/innerwindow_test.go +++ b/container/innerwindow_test.go @@ -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") diff --git a/theme/size.go b/theme/size.go index 012487b650..08ca3a8184 100644 --- a/theme/size.go +++ b/theme/size.go @@ -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. @@ -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 + } +} diff --git a/theme/theme.go b/theme/theme.go index f59b4b8d9d..ba6d80d9d7 100644 --- a/theme/theme.go +++ b/theme/theme.go @@ -146,43 +146,6 @@ func (t *builtinTheme) Font(style fyne.TextStyle) fyne.Resource { return t.regular } -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 - default: - return 0 - } -} - // Current returns the theme that is currently used for the running application. // It looks up based on user preferences and application configuration. //