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)) }