ui/window.go
Noah Petherbridge 07cefb6499 Menus and Menu Bars
* New and completed widgets: Menu, MenuButton and MenuBar.
* MenuButton is a kind of Button that opens a popup Menu when clicked.
* MenuBar is a container of buttons designed to be attached to the top
  of an application window ("File, Edit, View, Help")
* Supervisor manages the popup menus with its new concept of a Modal
  Widget. Modal widgets take exclusive event priority for all mouse and
  key events. The pop-up menu is a modal window, which means you must
  click an option inside the menu OR clicking outside the menu will
  close it and eat your click event (widgets outside the modal don't
  receive events, but the modal itself gets an event that you've done
  this).
2020-06-04 00:50:23 -07:00

360 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package ui
import (
"fmt"
"git.kirsle.net/go/render"
)
// 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.
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,
)
})
w.body.Configure(Config{
Background: render.Grey,
BorderSize: 2,
BorderStyle: BorderRaised,
})
// 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)
return w
}
// 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 (
bg = w.ActiveTitleBackground
fg = w.ActiveTitleForeground
)
if !w.focused {
bg = w.InactiveTitleBackground
fg = w.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)
}
}
// 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.content.Place(child, config)
}
// TitleBar returns the title bar widget.
func (w *Window) TitleBar() *Frame {
return w.titleBar
}
// 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.
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)
}
// 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)
}