443 lines
11 KiB
Go
443 lines
11 KiB
Go
package container
|
|
|
|
import (
|
|
"image/color"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
intWidget "fyne.io/fyne/v2/internal/widget"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"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
|
|
// a regular container/canvas.
|
|
//
|
|
// Since: 2.5
|
|
type InnerWindow struct {
|
|
widget.BaseWidget
|
|
|
|
CloseIntercept func() `json:"-"`
|
|
OnDragged, OnResized func(*fyne.DragEvent) `json:"-"`
|
|
OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func() `json:"-"`
|
|
Icon fyne.Resource
|
|
|
|
// Alignment allows an inner window to specify if the buttons should be on the left
|
|
// (`ButtonAlignLeading`) or right of the window border.
|
|
//
|
|
// Since: 2.6
|
|
Alignment widget.ButtonAlign
|
|
|
|
title string
|
|
content *fyne.Container
|
|
maximized bool
|
|
}
|
|
|
|
// 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: NewPadded(content)}
|
|
w.ExtendBaseWidget(w)
|
|
return w
|
|
}
|
|
|
|
func (w *InnerWindow) Close() {
|
|
w.Hide()
|
|
}
|
|
|
|
func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
|
|
w.ExtendBaseWidget(w)
|
|
th := w.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
min := newBorderButton(theme.WindowMinimizeIcon(), modeMinimize, th, w.OnMinimized)
|
|
if w.OnMinimized == nil {
|
|
min.Disable()
|
|
}
|
|
max := newBorderButton(theme.WindowMaximizeIcon(), modeMaximize, th, w.OnMaximized)
|
|
if w.OnMaximized == nil {
|
|
max.Disable()
|
|
}
|
|
|
|
close := newBorderButton(theme.WindowCloseIcon(), modeClose, th, func() {
|
|
if f := w.CloseIntercept; f != nil {
|
|
f()
|
|
} else {
|
|
w.Close()
|
|
}
|
|
})
|
|
buttons := NewCenter(NewHBox(close, min, max))
|
|
|
|
borderIcon := newBorderButton(w.Icon, modeIcon, th, func() {
|
|
if f := w.OnTappedIcon; f != nil {
|
|
f()
|
|
}
|
|
})
|
|
if w.OnTappedIcon == nil {
|
|
borderIcon.Disable()
|
|
}
|
|
|
|
if w.Icon == nil {
|
|
borderIcon.Hide()
|
|
}
|
|
title := newDraggableLabel(w.title, w)
|
|
title.Truncation = fyne.TextTruncateEllipsis
|
|
|
|
height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight)
|
|
off := (height - title.labelMinSize().Height) / 2
|
|
barMid := New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title)
|
|
if w.buttonPosition() == widget.ButtonAlignTrailing {
|
|
buttons = NewCenter(NewHBox(min, max, close))
|
|
}
|
|
|
|
bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
|
|
contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v))
|
|
corner := newDraggableCorner(w)
|
|
bar := New(&titleBarLayout{buttons: buttons, icon: borderIcon, title: barMid, win: w},
|
|
buttons, borderIcon, barMid)
|
|
|
|
if w.content == nil {
|
|
w.content = NewPadded(canvas.NewRectangle(color.Transparent))
|
|
}
|
|
objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner}
|
|
r := &innerWindowRenderer{ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel),
|
|
win: w, bar: bar, buttonBox: buttons, buttons: []*borderButton{close, min, max}, bg: bg,
|
|
corner: corner, contentBG: contentBG, icon: borderIcon}
|
|
r.Layout(w.Size())
|
|
return r
|
|
}
|
|
|
|
func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
|
|
w.content.Objects[0] = obj
|
|
|
|
w.content.Refresh()
|
|
}
|
|
|
|
// SetMaximized tells the window if the maximized state should be set or not.
|
|
//
|
|
// Since: 2.6
|
|
func (w *InnerWindow) SetMaximized(max bool) {
|
|
w.maximized = max
|
|
w.Refresh()
|
|
}
|
|
|
|
func (w *InnerWindow) SetPadded(pad bool) {
|
|
if pad {
|
|
w.content.Layout = layout.NewPaddedLayout()
|
|
} else {
|
|
w.content.Layout = layout.NewStackLayout()
|
|
}
|
|
w.content.Refresh()
|
|
}
|
|
|
|
func (w *InnerWindow) SetTitle(title string) {
|
|
w.title = title
|
|
w.Refresh()
|
|
}
|
|
|
|
func (w *InnerWindow) buttonPosition() widget.ButtonAlign {
|
|
if w.Alignment != widget.ButtonAlignCenter {
|
|
return w.Alignment
|
|
}
|
|
|
|
if runtime.GOOS == "windows" || runtime.GOOS == "linux" || strings.Contains(runtime.GOOS, "bsd") {
|
|
return widget.ButtonAlignTrailing
|
|
}
|
|
// macOS
|
|
return widget.ButtonAlignLeading
|
|
}
|
|
|
|
var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil)
|
|
|
|
type innerWindowRenderer struct {
|
|
*intWidget.ShadowingRenderer
|
|
|
|
win *InnerWindow
|
|
bar, buttonBox *fyne.Container
|
|
buttons []*borderButton
|
|
icon *borderButton
|
|
bg, contentBG *canvas.Rectangle
|
|
corner fyne.CanvasObject
|
|
}
|
|
|
|
func (i *innerWindowRenderer) Layout(size fyne.Size) {
|
|
th := i.win.Theme()
|
|
pad := th.Size(theme.SizeNamePadding)
|
|
|
|
i.LayoutShadow(size, fyne.Position{})
|
|
i.bg.Resize(size)
|
|
|
|
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 := 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(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()
|
|
barHeight := th.Size(theme.SizeNameWindowTitleBarHeight)
|
|
|
|
innerWidth := fyne.Max(i.bar.MinSize().Width, contentMin.Width)
|
|
|
|
return fyne.NewSize(innerWidth+pad*2, contentMin.Height+pad+barHeight)
|
|
}
|
|
|
|
func (i *innerWindowRenderer) Refresh() {
|
|
th := i.win.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
i.bg.FillColor = th.Color(theme.ColorNameOverlayBackground, v)
|
|
i.bg.Refresh()
|
|
i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v)
|
|
i.contentBG.Refresh()
|
|
|
|
if i.win.buttonPosition() == widget.ButtonAlignTrailing {
|
|
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[1], i.buttons[2], i.buttons[0]}
|
|
} else {
|
|
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[0], i.buttons[1], i.buttons[2]}
|
|
}
|
|
for _, b := range i.buttons {
|
|
b.setTheme(th)
|
|
}
|
|
i.bar.Refresh()
|
|
|
|
if i.win.OnMinimized == nil {
|
|
i.buttons[1].Disable()
|
|
} else {
|
|
i.buttons[1].SetOnTapped(i.win.OnMinimized)
|
|
i.buttons[1].Enable()
|
|
}
|
|
|
|
max := i.buttons[2]
|
|
if i.win.OnMaximized == nil {
|
|
i.buttons[2].Disable()
|
|
} else {
|
|
max.SetOnTapped(i.win.OnMaximized)
|
|
max.Enable()
|
|
}
|
|
if i.win.maximized {
|
|
max.b.SetIcon(theme.ViewRestoreIcon())
|
|
} else {
|
|
max.b.SetIcon(theme.WindowMaximizeIcon())
|
|
}
|
|
|
|
title := i.bar.Objects[2].(*fyne.Container).Objects[0].(*draggableLabel)
|
|
title.SetText(i.win.title)
|
|
i.ShadowingRenderer.RefreshShadow()
|
|
if i.win.OnTappedIcon == nil {
|
|
i.icon.Disable()
|
|
} else {
|
|
i.icon.Enable()
|
|
}
|
|
if i.win.Icon != nil {
|
|
i.icon.b.SetIcon(i.win.Icon)
|
|
i.icon.Show()
|
|
} else {
|
|
i.icon.Hide()
|
|
}
|
|
}
|
|
|
|
type draggableLabel struct {
|
|
widget.Label
|
|
win *InnerWindow
|
|
}
|
|
|
|
func newDraggableLabel(title string, win *InnerWindow) *draggableLabel {
|
|
d := &draggableLabel{win: win}
|
|
d.ExtendBaseWidget(d)
|
|
d.Text = title
|
|
return d
|
|
}
|
|
|
|
func (d *draggableLabel) Dragged(ev *fyne.DragEvent) {
|
|
if f := d.win.OnDragged; f != nil {
|
|
f(ev)
|
|
}
|
|
}
|
|
|
|
func (d *draggableLabel) DragEnd() {
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func newDraggableCorner(w *InnerWindow) *draggableCorner {
|
|
d := &draggableCorner{win: w}
|
|
d.ExtendBaseWidget(d)
|
|
return d
|
|
}
|
|
|
|
func (c *draggableCorner) CreateRenderer() fyne.WidgetRenderer {
|
|
prop := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon(theme.IconNameDragCornerIndicator))
|
|
prop.SetMinSize(fyne.NewSquareSize(16))
|
|
return widget.NewSimpleRenderer(prop)
|
|
}
|
|
|
|
func (c *draggableCorner) Dragged(ev *fyne.DragEvent) {
|
|
if f := c.win.OnResized; f != nil {
|
|
c.win.OnResized(ev)
|
|
}
|
|
}
|
|
|
|
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) Enable() {
|
|
b.b.Enable()
|
|
}
|
|
|
|
func (b *borderButton) SetOnTapped(fn func()) {
|
|
b.b.OnTapped = fn
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
type titleBarLayout struct {
|
|
win *InnerWindow
|
|
buttons, icon, title fyne.CanvasObject
|
|
}
|
|
|
|
func (t *titleBarLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
|
|
buttonMinWidth := t.buttons.MinSize().Width
|
|
t.buttons.Resize(fyne.NewSize(buttonMinWidth, s.Height))
|
|
t.icon.Resize(fyne.NewSquareSize(s.Height))
|
|
usedWidth := buttonMinWidth
|
|
if t.icon.Visible() {
|
|
usedWidth += s.Height
|
|
}
|
|
t.title.Resize(fyne.NewSize(s.Width-usedWidth, s.Height))
|
|
|
|
if t.win.buttonPosition() == widget.ButtonAlignTrailing {
|
|
t.buttons.Move(fyne.NewPos(s.Width-buttonMinWidth, 0))
|
|
t.icon.Move(fyne.Position{})
|
|
if t.icon.Visible() {
|
|
t.title.Move(fyne.NewPos(s.Height, 0))
|
|
} else {
|
|
t.title.Move(fyne.Position{})
|
|
}
|
|
} else {
|
|
t.buttons.Move(fyne.NewPos(0, 0))
|
|
t.icon.Move(fyne.NewPos(s.Width-s.Height, 0))
|
|
t.title.Move(fyne.NewPos(buttonMinWidth, 0))
|
|
}
|
|
}
|
|
|
|
func (t *titleBarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
|
|
buttonMin := t.buttons.MinSize()
|
|
iconMin := t.icon.MinSize()
|
|
titleMin := t.title.MinSize() // can truncate
|
|
|
|
return fyne.NewSize(buttonMin.Width+iconMin.Width+titleMin.Width,
|
|
fyne.Max(fyne.Max(buttonMin.Height, iconMin.Height), titleMin.Height))
|
|
}
|