ui/window.go

398 lines
9.4 KiB
Go
Raw Normal View History

package ui
import (
"fmt"
"git.kirsle.net/go/render"
2020-06-18 01:04:18 +00:00
"git.kirsle.net/go/ui/style"
)
// Window is a frame with a title bar.
type Window struct {
BaseWidget
Title string
// Title bar colors. Sensible defaults are chosen in NewWindow but you
// may customize after the fact.
ActiveTitleBackground render.Color
ActiveTitleForeground render.Color
InactiveTitleBackground render.Color
InactiveTitleForeground render.Color
// Private widgets.
2020-06-18 01:04:18 +00:00
style *style.Window
body *Frame
titleBar *Frame
titleLabel *Label
titleButtons []*Button
content *Frame
// Configured title bar buttons.
buttonsEnabled int
// Window manager controls.
dragging bool
startDragAt render.Point // cursor position when drag began
dragOrigPoint render.Point // original position of window at drag start
focused bool
managed bool // window is managed by Supervisor
maximized bool // toggled by MaximizeButton
origPoint render.Point // placement before a maximize
origSize render.Rect // size before a maximize
engine render.Engine // hang onto the render engine, for Maximize support.
}
// NewWindow creates a new window.
func NewWindow(title string) *Window {
w := &Window{
Title: title,
body: NewFrame("body:" + title),
// Default title bar colors.
ActiveTitleBackground: render.Blue,
ActiveTitleForeground: render.White,
InactiveTitleBackground: render.DarkGrey,
InactiveTitleForeground: render.Grey,
}
w.IDFunc(func() string {
return fmt.Sprintf("Window<%s %+v>",
w.Title, w.focused,
)
})
// Title bar widget.
titleBar, titleLabel := w.setupTitleBar()
w.body.Pack(titleBar, Pack{
Side: N,
Fill: true,
})
w.titleBar = titleBar
w.titleLabel = titleLabel
// Window content frame.
content := NewFrame("content:" + title)
content.Configure(Config{
Background: render.Grey,
})
w.body.Pack(content, Pack{
Side: N,
Fill: true,
})
w.content = content
// Set up parent/child relationships
w.body.SetParent(w)
2020-06-18 01:04:18 +00:00
w.SetStyle(Theme.Window)
return w
}
2020-06-18 01:04:18 +00:00
// SetStyle sets the window style.
func (w *Window) SetStyle(v *style.Window) {
if v == nil {
v = &style.DefaultWindow
}
w.style = v
w.body.Configure(Config{
Background: w.style.ActiveBackground,
BorderSize: 2,
BorderStyle: BorderRaised,
})
if w.focused {
w.titleBar.SetBackground(w.style.ActiveTitleBackground)
w.titleLabel.Font.Color = w.style.ActiveTitleForeground
} else {
w.titleBar.SetBackground(w.style.InactiveTitleBackground)
w.titleLabel.Font.Color = w.style.InactiveTitleForeground
}
}
// setupTitlebar creates the title bar frame of the window.
func (w *Window) setupTitleBar() (*Frame, *Label) {
frame := NewFrame("Titlebar for Window: " + w.Title)
frame.Configure(Config{
Background: w.ActiveTitleBackground,
})
// Title label.
label := NewLabel(Label{
TextVariable: &w.Title,
Font: render.Text{
Color: w.ActiveTitleForeground,
Size: 10,
Stroke: w.ActiveTitleBackground.Darken(40),
Padding: 2,
},
})
frame.Pack(label, Pack{
Side: W,
})
// Window buttons.
var buttons = []struct {
If bool
Label string
Event Event
}{
{
Label: "×",
Event: CloseWindow,
},
{
Label: "+",
Event: MaximizeWindow,
},
{
Label: "_",
Event: MinimizeWindow,
},
}
w.titleButtons = make([]*Button, len(buttons))
for i, cfg := range buttons {
cfg := cfg
btn := NewButton(
fmt.Sprintf("Title Button %d for Window: %s", i, w.Title),
NewLabel(Label{
Text: cfg.Label,
Font: render.Text{
// Color: w.ActiveTitleForeground,
Size: 8,
Padding: 2,
},
}),
)
btn.SetBorderSize(0)
btn.Handle(Click, func(ed EventData) error {
w.Event(cfg.Event, ed)
return ErrStopPropagation // TODO: doesn't work :(
})
btn.Hide()
w.titleButtons[i] = btn
frame.Pack(btn, Pack{
Side: E,
})
}
return frame, label
}
// SetButtons sets the title bar buttons to show in the window.
//
// The value should be the OR of CloseButton, MaximizeButton and MinimizeButton
// that you want to be enabled.
//
// Window buttons only work if the window is managed by Supervisor and you have
// called the Supervise() method of the window.
func (w *Window) SetButtons(buttons int) {
// Show/hide each button based on the value given.
var toggle = []struct {
Value int
Index int
}{
{
Value: CloseButton,
Index: 0,
},
{
Value: MaximizeButton,
Index: 1,
},
{
Value: MinimizeButton,
Index: 2,
},
}
for _, item := range toggle {
if buttons&item.Value == item.Value {
w.titleButtons[item.Index].Show()
} else {
w.titleButtons[item.Index].Hide()
}
}
}
// Supervise enables the window to be dragged around by its title bar by
// adding its relevant event hooks to your Supervisor.
func (w *Window) Supervise(s *Supervisor) {
// Add a click handler to the title bar to enable dragging.
w.titleBar.Handle(MouseDown, func(ed EventData) error {
w.startDragAt = ed.Point
w.dragOrigPoint = w.Point()
s.DragStartWidget(w)
return nil
})
// Clicking anywhere in the window focuses the window.
w.Handle(MouseDown, func(ed EventData) error {
s.FocusWindow(w)
return nil
})
// Window as a whole receives DragMove events while being dragged.
w.Handle(DragMove, func(ed EventData) error {
// Get the delta of movement from where we began.
delta := w.startDragAt.Compare(ed.Point)
if delta != render.Origin {
moveTo := w.dragOrigPoint
moveTo.Add(delta)
w.MoveTo(moveTo)
}
return nil
})
// Window button handlers.
w.Handle(CloseWindow, func(ed EventData) error {
w.Hide()
return nil
})
w.Handle(MaximizeWindow, func(ed EventData) error {
w.SetMaximized(!w.maximized)
return nil
})
// Add the title bar to the supervisor.
s.Add(w.titleBar)
for _, btn := range w.titleButtons {
s.Add(btn)
}
s.Add(w)
// Add the window to the focus list of the supervisor.
s.addWindow(w)
}
// Focused returns whether the window is focused.
func (w *Window) Focused() bool {
return w.focused
}
// SetFocus sets the window's focus value. Note: if you're using the Supervisor
// to manage the windows, do NOT call this method -- window focus is managed
// by the Supervisor.
func (w *Window) SetFocus(v bool) {
w.focused = v
// Update the title bar colors.
var (
2020-06-18 01:04:18 +00:00
bg = w.style.ActiveTitleBackground
fg = w.style.ActiveTitleForeground
)
if !w.focused {
2020-06-18 01:04:18 +00:00
bg = w.style.InactiveTitleBackground
fg = w.style.InactiveTitleForeground
}
w.titleBar.SetBackground(bg)
w.titleLabel.Font.Color = fg
w.titleLabel.Font.Stroke = bg.Darken(40)
}
// Maximized returns whether the window is maximized.
func (w *Window) Maximized() bool {
return w.maximized
}
// SetMaximized sets the state of the maximized window.
// Must have called Compute() once before so the window can hang on to the
// render.Engine, to calculate the size of the parent window.
func (w *Window) SetMaximized(v bool) {
w.maximized = v
if v && w.engine != nil {
w.origPoint = w.Point()
w.origSize = w.Size()
w.MoveTo(render.Origin)
w.Resize(render.NewRect(w.engine.WindowSize()))
w.Compute(w.engine)
} else if w.engine != nil {
w.MoveTo(w.origPoint)
w.Resize(w.origSize)
w.Compute(w.engine)
}
}
// Size returns the window's size (the size of its underlying body frame,
// including its title bar and content frames).
func (w *Window) Size() render.Rect {
return w.body.Size()
}
// Close the window, hiding it from display and calling its CloseWindow handler.
func (w *Window) Close() {
w.Hide()
w.Event(CloseWindow, EventData{})
}
// Children returns the window's child widgets.
func (w *Window) Children() []Widget {
return []Widget{
w.body,
}
}
// Pack a child widget into the window's main frame.
func (w *Window) Pack(child Widget, config ...Pack) {
w.content.Pack(child, config...)
}
// Place a child widget into the window's main frame.
func (w *Window) Place(child Widget, config Place) {
w.body.Place(child, config)
}
2020-07-10 02:29:44 +00:00
// TitleBar returns the title bar widgets.
func (w *Window) TitleBar() (*Frame, *Label) {
return w.titleBar, w.titleLabel
}
// Configure the widget. Color and style changes are passed down to the inner
// content frame of the window.
func (w *Window) Configure(C Config) {
w.BaseWidget.Configure(C)
w.body.Configure(C)
// Don't pass dimensions down any further than the body.
// TODO: this causes the content frame to compute its size
// dynamically based on Packed widgets, but if using Place on
// your window, the content frame doesn't know a size by which
// to place the child relative to (Frame has size 0x0).
// Commenting out these two lines causes windows to render very
// incorrectly (child frame content flying off the window bottom).
// In the meantime, Window.Place intercepts it and draws it onto
// the parent window directly so it works how you expect.
C.Width = 0
C.Height = 0
w.content.Configure(C)
}
// ConfigureTitle configures the title bar widget.
func (w *Window) ConfigureTitle(C Config) {
w.titleBar.Configure(C)
}
// ContentFrame returns the main content Frame of this window.
func (w *Window) ContentFrame() *Frame {
return w.content
}
// Compute the window.
func (w *Window) Compute(e render.Engine) {
w.engine = e // hang onto it in case of maximize
w.body.Compute(e)
// Call the BaseWidget Compute in case we have subscribers.
w.BaseWidget.Compute(e)
}
// Present the window.
func (w *Window) Present(e render.Engine, P render.Point) {
w.body.Present(e, P)
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
}