Browse Source

Window Manager Basics, Work in Progress

* Adds Window Manager support to the Supervisor, so that Window widgets
  can be dragged by their title bar, clicked to focus, etc.
* Create a ui.Window as normal, but instead of Packing or Placing it
  into a parent container as before, you call .Supervise() and give it
  your Supervisor. The window registers itself to be managed and drawn
  by the Supervisor itself.
* Supervisor manages the focused window order using a doubly linked
  list. When a window takes focus it moves to the top of the list.
  Widgets in the active window take event priority.
* Extended DragDrop API to support holding a widget pointer in the drag
  operation.
* Changed widget event Handle functions to return an error: so that they
  could return ErrStopPropagation to prevent events going to more
  widgets once handled (for important events).

Some bugs remain around overlapping windows and event propagation.
menus
Noah Petherbridge 4 months ago
parent
commit
7d9ba79cd2
14 changed files with 690 additions and 107 deletions
  1. +8
    -4
      button.go
  2. +10
    -5
      check_button.go
  3. +2
    -2
      checkbox.go
  4. +16
    -1
      dragdrop.go
  5. +94
    -0
      eg/windows/main.go
  6. +48
    -0
      functions.go
  7. +2
    -0
      go.mod
  8. +31
    -6
      main_window.go
  9. +2
    -1
      menu.go
  10. +179
    -53
      supervisor.go
  11. +8
    -4
      tooltip.go
  12. +11
    -8
      widget.go
  13. +131
    -23
      window.go
  14. +148
    -0
      window_manager.go

+ 8
- 4
button.go View File

@@ -35,22 +35,26 @@ func NewButton(name string, child Widget) *Button {
Background: theme.ButtonBackgroundColor,
})

w.Handle(MouseOver, func(e EventData) {
w.Handle(MouseOver, func(e EventData) error {
w.hovering = true
w.SetBackground(theme.ButtonHoverColor)
return nil
})
w.Handle(MouseOut, func(e EventData) {
w.Handle(MouseOut, func(e EventData) error {
w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor)
return nil
})

w.Handle(MouseDown, func(e EventData) {
w.Handle(MouseDown, func(e EventData) error {
w.clicked = true
w.SetBorderStyle(BorderSunken)
return nil
})
w.Handle(MouseUp, func(e EventData) {
w.Handle(MouseUp, func(e EventData) error {
w.clicked = false
w.SetBorderStyle(BorderRaised)
return nil
})

return w


+ 10
- 5
check_button.go View File

@@ -78,24 +78,28 @@ func (w *CheckButton) setup() {
Background: theme.ButtonBackgroundColor,
})

w.Handle(MouseOver, func(ed EventData) {
w.Handle(MouseOver, func(ed EventData) error {
w.hovering = true
w.SetBackground(theme.ButtonHoverColor)
return nil
})
w.Handle(MouseOut, func(ed EventData) {
w.Handle(MouseOut, func(ed EventData) error {
w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor)
return nil
})

w.Handle(MouseDown, func(ed EventData) {
w.Handle(MouseDown, func(ed EventData) error {
w.clicked = true
w.SetBorderStyle(BorderSunken)
return nil
})
w.Handle(MouseUp, func(ed EventData) {
w.Handle(MouseUp, func(ed EventData) error {
w.clicked = false
return nil
})

w.Handle(Click, func(ed EventData) {
w.Handle(Click, func(ed EventData) error {
var sunken bool
if w.BoolVar != nil {
if *w.BoolVar {
@@ -114,5 +118,6 @@ func (w *CheckButton) setup() {
} else {
w.SetBorderStyle(BorderRaised)
}
return nil
})
}

+ 2
- 2
checkbox.go View File

@@ -35,8 +35,8 @@ func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, c
// Forward clicks on the child widget to the CheckButton.
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
func(e Event) {
w.child.Handle(e, func(ed EventData) {
w.button.Event(e, ed)
w.child.Handle(e, func(ed EventData) error {
return w.button.Event(e, ed)
})
}(e)
}


+ 16
- 1
dragdrop.go View File

@@ -3,6 +3,9 @@ package ui
// DragDrop is a state machine to manage draggable UI components.
type DragDrop struct {
isDragging bool

// If the subject of the drag is a widget, it can store itself here.
widget Widget
}

// NewDragDrop initializes the DragDrop struct. Normally your Supervisor
@@ -17,12 +20,24 @@ func (dd *DragDrop) IsDragging() bool {
return dd.isDragging
}

// SetWidget attaches the widget to the drag state, but does not start the
// drag; you call Start() after this if the subject is a widget.
func (dd *DragDrop) SetWidget(w Widget) {
dd.widget = w
}

// Widget returns the attached widget or nil.
func (dd *DragDrop) Widget() Widget {
return dd.widget
}

// Start the drag state.
func (dd *DragDrop) Start() {
dd.isDragging = true
}

// Stop dragging.
// Stop dragging. This will also clear the stored widget, if any.
func (dd *DragDrop) Stop() {
dd.isDragging = false
dd.widget = nil
}

+ 94
- 0
eg/windows/main.go View File

@@ -0,0 +1,94 @@
package main

import (
"os"

"git.kirsle.net/go/render"
"git.kirsle.net/go/render/event"
"git.kirsle.net/go/render/sdl"
"git.kirsle.net/go/ui"
)

// Program globals.
var (
// Size of the MainWindow.
Width = 1024
Height = 768

// Cascade offset for creating multiple windows.
Cascade = render.NewPoint(10, 10)
CascadeStep = render.NewPoint(24, 24)
)

func init() {
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
}

func main() {
mw, err := ui.NewMainWindow("Hello World", Width, Height)
if err != nil {
panic(err)
}

// Add some windows to play with.
addWindow(mw, "First window")
addWindow(mw, "Second window")

mw.SetBackground(render.White)

mw.OnLoop(func(e *event.State) {
if e.Escape {
os.Exit(0)
}
})

mw.MainLoop()
}

// Add a new child window.
func addWindow(mw *ui.MainWindow, title string) {
win1 := ui.NewWindow(title)
win1.Configure(ui.Config{
Width: 640,
Height: 480,
})
win1.Compute(mw.Engine)
win1.Supervise(mw.Supervisor())

// Attach it to the MainWindow with no placement management, i.e.
// instead of Pack() or Place(). Since draggable windows set their own
// position, a position manager would only interfere and "snap" the
// window back into place as soon as you drop the title bar!
// mw.Attach(win1)

// Default placement via cascade.
win1.MoveTo(Cascade)
Cascade.Add(CascadeStep)

// Add a button to the window.
// btn := ui.NewButton("Button1", ui.NewLabel(ui.Label{
// Text: "Click me!",
// }))
// btn.Handle(ui.Click, func(ed ui.EventData) {
// fmt.Printf("Window '%s' button clicked!\n", title)
// })
// mw.Add(btn)
// win1.Place(btn, ui.Place{
// Top: 10,
// Left: 10,
// })

// Add a window duplicator button.
btn2 := ui.NewButton(title+":Button2", ui.NewLabel(ui.Label{
Text: "New Window",
}))
btn2.Handle(ui.Click, func(ed ui.EventData) error {
addWindow(mw, "New Window")
return nil
})
mw.Add(btn2)
win1.Place(btn2, ui.Place{
Top: 10,
Right: 10,
})
}

+ 48
- 0
functions.go View File

@@ -38,3 +38,51 @@ func AbsoluteRect(w Widget) render.Rect {
// below the status bar if we do `+ R.Y` here.
}
}

// widgetInFocusedWindow returns whether a widget (like a Button) is a
// descendant of a Window that is being Window Managed by Supervisor, and
// said window is in a Focused state.
//
// This is used by Supervisor to decide whether the widget should be given
// events or not: a widget in a non-focused window ignores events, so that a
// button in a "lower" window could not be clicked through a "higher" window
// that overlaps it.
func widgetInFocusedWindow(w Widget) (isManaged, isFocused bool) {
var node = w

for {
// Is the node a Window?
if window, ok := node.(*Window); ok {
return true, window.Focused()
}

node, _ = node.Parent()
if node == nil {
return false, true // reached the root
}
}
}

// WidgetInManagedWindow returns true if the widget is owned by a ui.Window
// which is being Window Managed by the Supervisor.
//
// Returns true if any parent widget is a Window with managed=true. This
// boolean is set when you call .Supervise() on the window to be managed by
// Supervisor.
func WidgetInManagedWindow(w Widget) bool {
var node = w

for {
// Is the node a Window?
if window, ok := node.(*Window); ok {
if window.managed {
return true
}
}

node, _ = node.Parent()
if node == nil {
return false // reached the root
}
}
}

+ 2
- 0
go.mod View File

@@ -2,4 +2,6 @@ module git.kirsle.net/go/ui

go 1.13

replace git.kirsle.net/go/render => /home/kirsle/go/src/git.kirsle.net/go/render

require git.kirsle.net/go/render v0.0.0-20200102014411-4d008b5c468d

+ 31
- 6
main_window.go View File

@@ -81,7 +81,8 @@ func (mw *MainWindow) SetTitle(title string) {
mw.Engine.SetTitle(title)
}

// Add a child widget to the window.
// Add a child widget to the window's supervisor. This alone does not make the
// child widget render each frame; use Pack, Place or Attach for that.
func (mw *MainWindow) Add(w Widget) {
mw.supervisor.Add(w)
}
@@ -98,11 +99,38 @@ func (mw *MainWindow) Place(w Widget, config Place) {
mw.frame.Place(w, config)
}

// Attach a child widget to the window without its position managed. The
// widget's Present() method will be called each time the window Presents, but
// the positioning of the child widget must be handled manually by the caller.
//
// Pack and Place are usually the methods you want to use to put a child widget
// into the window. One example use case for Attach is when you want to create
// child Window widgets which can be dragged by their title bars; their dynamic
// drag-drop positioning is best managed manually, and Pack or Place would
// interfere with their positioning otherwise.
//
// This also calls .Add() to add the widget to the MainWindow's Supervisor.
//
// Implementation details:
// - Adds the widget to the MainWindow's Supervisor.
// - Calls Frame.Add(w) so it will Present each time the main frame Presents.
// - Calls w.Compute() on your widget so it can calculate its initial size.
func (mw *MainWindow) Attach(w Widget) {
mw.Add(w)
mw.frame.Add(w)
w.Compute(mw.Engine)
}

// Frame returns the window's main frame, if needed.
func (mw *MainWindow) Frame() *Frame {
return mw.frame
}

// Supervisor returns the window's Supervisor instance.
func (mw *MainWindow) Supervisor() *Supervisor {
return mw.supervisor
}

// resized handles the window being resized.
func (mw *MainWindow) resized() {
mw.frame.Resize(render.Rect{
@@ -116,11 +144,6 @@ func (mw *MainWindow) SetBackground(color render.Color) {
mw.frame.SetBackground(color)
}

// Present the window.
func (mw *MainWindow) Present() {
mw.supervisor.Present(mw.Engine)
}

// OnLoop registers a function to be called on every loop of the main window.
// This enables your application to register global event handlers or whatnot.
// The function is called between the event polling and the updating of any UI
@@ -141,6 +164,7 @@ func (mw *MainWindow) MainLoop() error {

// Loop does one loop of the UI.
func (mw *MainWindow) Loop() error {
fmt.Printf("------ MAIN LOOP\n")
mw.Engine.Clear(render.White)

// Record how long this loop took.
@@ -171,6 +195,7 @@ func (mw *MainWindow) Loop() error {
// Render the child widgets.
mw.supervisor.Loop(ev)
mw.frame.Present(mw.Engine, mw.frame.Point())
mw.supervisor.Present(mw.Engine)
mw.Engine.Present()

// Delay to maintain target frames per second.


+ 2
- 1
menu.go View File

@@ -96,8 +96,9 @@ func NewMenuItem(label string, command func()) *MenuItem {
Background: render.Blue,
})

w.Button.Handle(Click, func(ed EventData) {
w.Button.Handle(Click, func(ed EventData) error {
w.Command()
return nil
})

// Assign the button


+ 179
- 53
supervisor.go View File

@@ -2,7 +2,6 @@ package ui

import (
"errors"
"fmt"
"sync"

"git.kirsle.net/go/render"
@@ -23,7 +22,13 @@ const (
KeyDown
KeyUp
KeyPress
Drop

// Drag/drop event handlers.
DragStop // if a widget is being dragged and the drag is done
DragMove // mouse movements sent to a widget being dragged.
Drop // a "drop site" widget under the cursor when a drag is done

// Lifecycle event handlers.
Compute // fired whenever the widget runs Compute
Present // fired whenever the widget runs Present
)
@@ -45,8 +50,13 @@ type Supervisor struct {
serial int // ID number of each widget added in order
widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
hovering map[int]interface{} // map of widgets under the cursor
clicked map[int]interface{} // map of widgets being clicked
clicked map[int]bool // map of widgets being clicked
dd *DragDrop

// List of window focus history for Window Manager.
winFocus *FocusedWindow
winTop *FocusedWindow // pointer to top-most window
winBottom *FocusedWindow // pointer to bottom-most window
}

// WidgetSlot holds a widget with a unique ID number in a sorted list.
@@ -60,16 +70,30 @@ func NewSupervisor() *Supervisor {
return &Supervisor{
widgets: map[int]WidgetSlot{},
hovering: map[int]interface{}{},
clicked: map[int]interface{}{},
clicked: map[int]bool{},
dd: NewDragDrop(),
}
}

// DragStart sets the drag state.
// DragStart sets the drag state without a widget.
//
// An example where you'd use this is if you want a widget to respond to a
// Drop event (mouse released over a drop-site widget) but the 'thing' being
// dragged is not a ui.Widget, i.e., for custom app specific logic.
func (s *Supervisor) DragStart() {
s.dd.Start()
}

// DragStartWidget sets the drag state to true with a target widget attached.
//
// The widget being dragged is given DragMove events while the drag is
// underway. When the mouse button is released, the widget is given a
// DragStop event and the widget below the cursor is given a Drop event.
func (s *Supervisor) DragStartWidget(w Widget) {
s.dd.SetWidget(w)
s.dd.Start()
}

// DragStop stops the drag state.
func (s *Supervisor) DragStop() {
s.dd.Stop()
@@ -85,6 +109,7 @@ var (
// The caller should STOP forwarding any mouse or keyboard events to any
// other handles for the remainder of this tick.
ErrStopPropagation = errors.New("stop all event propagation")
ErrNoEventHandler = errors.New("no event handler")
)

// Loop to check events and pass them to managed widgets.
@@ -113,11 +138,109 @@ func (s *Supervisor) Loop(ev *event.State) error {
})
}
s.DragStop()
} else {
// If we have a target widget being dragged, send it mouse events.
if target := s.dd.Widget(); target != nil {
target.Event(DragMove, EventData{
Point: XY,
})
}
}
return ErrStopPropagation
}

// Run events in managed windows first, from top to bottom.
// Widgets in unmanaged windows will be handled next.
// err := s.runWindowEvents(XY, ev, hovering, outside)
handled, err := s.runWidgetEvents(XY, ev, hovering, outside, true)
if err == ErrStopPropagation || handled {
// A widget in the active window has accepted an event. Do not pass
// the event also to lower widgets.
return nil
}

// Run events for the other widgets not in a managed window.
s.runWidgetEvents(XY, ev, hovering, outside, false)

return nil
}

// Hovering returns all of the widgets managed by Supervisor that are under
// the mouse cursor. Returns the set of widgets below the cursor and the set
// of widgets not below the cursor.
func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSlot) {
var XY = cursor // for shorthand
hovering = []WidgetSlot{}
outside = []WidgetSlot{}

// Check all the widgets under our care.
for child := range s.Widgets() {
var (
w = child.widget
P = AbsolutePosition(w)
S = w.Size()
P2 = render.Point{
X: P.X + S.W,
Y: P.Y + S.H,
}
)

if XY.X >= P.X && XY.X < P2.X && XY.Y >= P.Y && XY.Y < P2.Y {
// Cursor intersects the widget.
hovering = append(hovering, child)
} else {
outside = append(outside, child)
}
}

return hovering, outside
}

// runWindowEvents is a subroutine of Supervisor.Loop().
//
// After determining the widgets below the cursor (hovering) and outside the
// cursor, transmit mouse events to the widgets.
//
// This function has two use cases:
// - In runWindowEvents where we run events for the top-most focused window of
// the window manager.
// - In Supervisor.Loop() for the widgets that are NOT owned by a managed
// window, so that these widgets always get events.
//
// Parameters:
// XY (Point): mouse cursor position as calculated in Loop()
// ev, hovering, outside: values from Loop(), self explanatory.
// behavior: indicates how this method is being used.
//
// behavior options:
// 0: widgets NOT part of a managed window. On this pass, if a widget IS
// a part of a window, it gets no events triggered.
// 1: widgets are part of the active focused window.
func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State, hovering, outside []WidgetSlot, toFocusedWindow bool) (bool, error) {
// Do we run any events?
var (
stopPropagation bool
ranEvents bool
)

// Handler for an Event response errors.
handle := func(err error) {
// Did any event handler run?
if err != ErrNoEventHandler {
ranEvents = true
}

// Are we stopping propagation?
if err == ErrStopPropagation {
stopPropagation = true
}
}

for _, child := range hovering {
if stopPropagation {
break
}

var (
id = child.id
w = child.widget
@@ -128,29 +251,54 @@ func (s *Supervisor) Loop(ev *event.State) error {
continue
}

// Check if the widget is part of a Window managed by Supervisor.
isManaged, isFocused := widgetInFocusedWindow(w)

// Are we sending events to it?
if toFocusedWindow {
// Only sending events to widgets owned by the focused window.
if !(isManaged && isFocused) {
continue
}
} else {
// Sending only to widgets NOT managed by a window. This can include
// Window widgets themselves, so lower unfocused windows may be
// brought to foreground.
window, isWindow := w.(*Window)
if isManaged && !isWindow {
continue
}

// It is a window, but can only be the non-focused window.
if window.focused {
continue
}
}

// Cursor has intersected the widget.
if _, ok := s.hovering[id]; !ok {
w.Event(MouseOver, EventData{
handle(w.Event(MouseOver, EventData{
Point: XY,
})
}))
s.hovering[id] = nil
}

_, isClicked := s.clicked[id]
isClicked, _ := s.clicked[id]
if ev.Button1 {
if !isClicked {
w.Event(MouseDown, EventData{
err := w.Event(MouseDown, EventData{
Point: XY,
})
s.clicked[id] = nil
handle(err)
s.clicked[id] = true
}
} else if isClicked {
w.Event(MouseUp, EventData{
handle(w.Event(MouseUp, EventData{
Point: XY,
})
w.Event(Click, EventData{
}))
handle(w.Event(Click, EventData{
Point: XY,
})
}))
delete(s.clicked, id)
}
}
@@ -162,52 +310,27 @@ func (s *Supervisor) Loop(ev *event.State) error {

// Cursor is not intersecting the widget.
if _, ok := s.hovering[id]; ok {
w.Event(MouseOut, EventData{
handle(w.Event(MouseOut, EventData{
Point: XY,
})
}))
delete(s.hovering, id)
}

if _, ok := s.clicked[id]; ok {
w.Event(MouseUp, EventData{
handle(w.Event(MouseUp, EventData{
Point: XY,
})
}))
delete(s.clicked, id)
}
}

return nil
}

// Hovering returns all of the widgets managed by Supervisor that are under
// the mouse cursor. Returns the set of widgets below the cursor and the set
// of widgets not below the cursor.
func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSlot) {
var XY = cursor // for shorthand
hovering = []WidgetSlot{}
outside = []WidgetSlot{}

// Check all the widgets under our care.
for child := range s.Widgets() {
var (
w = child.widget
P = AbsolutePosition(w)
S = w.Size()
P2 = render.Point{
X: P.X + S.W,
Y: P.Y + S.H,
}
)

if XY.X >= P.X && XY.X < P2.X && XY.Y >= P.Y && XY.Y < P2.Y {
// Cursor intersects the widget.
hovering = append(hovering, child)
} else {
outside = append(outside, child)
}
// If a stopPropagation was called, return it up the stack.
if stopPropagation {
return ranEvents, ErrStopPropagation
}

return hovering, outside
// If ANY event handler was called, return nil to signal
return ranEvents, nil
}

// Widgets returns a channel of widgets managed by the supervisor in the order
@@ -226,15 +349,18 @@ func (s *Supervisor) Widgets() <-chan WidgetSlot {
}

// Present all widgets managed by the supervisor.
//
// NOTE: only the Window Manager feature uses this method, and this method
// will render the windows from bottom to top with the focused window on top.
// For other widgets, they should be added to a parent Frame that will call
// Present on them each time the parent Presents, or otherwise you need to
// manage the presentation of widgets outside the Supervisor.
func (s *Supervisor) Present(e render.Engine) {
s.lock.RLock()
defer s.lock.RUnlock()

fmt.Println("!!! ui.Supervisor.Present() is deprecated")
// for child := range s.Widgets() {
// var w = child.widget
// w.Present(e, w.Point())
// }
// Render the window manager windows from bottom to top.
s.presentWindows(e)
}

// Add a widget to be supervised.


+ 8
- 4
tooltip.go View File

@@ -53,17 +53,21 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
// - Hide it on MouseOut
// - Compute the tooltip when the parent widget Computes
// - Present the tooltip when the parent widget Presents
target.Handle(MouseOver, func(ed EventData) {
target.Handle(MouseOver, func(ed EventData) error {
w.Show()
return nil
})
target.Handle(MouseOut, func(ed EventData) {
target.Handle(MouseOut, func(ed EventData) error {
w.Hide()
return nil
})
target.Handle(Compute, func(ed EventData) {
target.Handle(Compute, func(ed EventData) error {
w.Compute(ed.Engine)
return nil
})
target.Handle(Present, func(ed EventData) {
target.Handle(Present, func(ed EventData) error {
w.Present(ed.Engine, w.Point())
return nil
})

w.IDFunc(func() string {


+ 11
- 8
widget.go View File

@@ -32,8 +32,8 @@ type Widget interface {
ResizeAuto(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()

Handle(Event, func(EventData))
Event(Event, EventData) // called internally to trigger an event
Handle(Event, func(EventData) error)
Event(Event, EventData) error // called internally to trigger an event

// Thickness of the padding + border + outline.
BoxThickness(multiplier int) int
@@ -117,7 +117,7 @@ type BaseWidget struct {
borderSize int
outlineColor render.Color
outlineSize int
handlers map[Event][]func(EventData)
handlers map[Event][]func(EventData) error
hasParent bool
parent Widget
}
@@ -489,22 +489,25 @@ func (w *BaseWidget) Present(e render.Engine, p render.Point) {
}

// Event is called internally by Doodle to trigger an event.
func (w *BaseWidget) Event(event Event, e EventData) {
// Handlers can return ErrStopPropagation to prevent further widgets being
// notified of events.
func (w *BaseWidget) Event(event Event, e EventData) error {
if handlers, ok := w.handlers[event]; ok {
for _, fn := range handlers {
fn(e)
return fn(e)
}
}
return ErrNoEventHandler
}

// Handle an event in the widget.
func (w *BaseWidget) Handle(event Event, fn func(EventData)) {
func (w *BaseWidget) Handle(event Event, fn func(EventData) error) {
if w.handlers == nil {
w.handlers = map[Event][]func(EventData){}
w.handlers = map[Event][]func(EventData) error{}
}

if _, ok := w.handlers[event]; !ok {
w.handlers[event] = []func(EventData){}
w.handlers[event] = []func(EventData) error{}
}

w.handlers[event] = append(w.handlers[event], fn)


+ 131
- 23
window.go View File

@@ -12,10 +12,25 @@ type Window struct {
Title string
Active bool

// 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 *Label
content *Frame
body *Frame
titleBar *Frame
titleLabel *Label
content *Frame

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

// NewWindow creates a new window.
@@ -23,10 +38,16 @@ 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.Grey,
InactiveTitleForeground: render.Black,
}
w.IDFunc(func() string {
return fmt.Sprintf("Window<%s>",
w.Title,
return fmt.Sprintf("Window<%s %+v>",
w.Title, w.focused,
)
})

@@ -37,23 +58,13 @@ func NewWindow(title string) *Window {
})

// Title bar widget.
titleBar := NewLabel(Label{
TextVariable: &w.Title,
Font: render.Text{
Color: render.White,
Size: 10,
Stroke: render.DarkBlue,
Padding: 2,
},
})
titleBar.Configure(Config{
Background: render.Blue,
})
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)
@@ -72,6 +83,98 @@ func NewWindow(title string) *Window {
return w
}

// setupTitlebar creates the title bar frame of the window.
func (w *Window) setupTitleBar() (*Frame, *Label) {
frame := NewFrame("Titlebar for Windows: " + w.Title)
frame.Configure(Config{
Background: w.ActiveTitleBackground,
})

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

return frame, label
}

// 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()
fmt.Printf("Clicked at %s window at %s!\n", ed.Point, w.dragOrigPoint)

s.DragStartWidget(w)
return nil
})

// Clicking anywhere in the window focuses the window.
w.Handle(MouseDown, func(ed EventData) error {
s.FocusWindow(w)
fmt.Printf("%s handles click event\n", w)
return nil
})
w.Handle(Click, func(ed EventData) error {
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 {
fmt.Printf(" Dragged to: %s Delta: %s\n", ed.Point, delta)
moveTo := w.dragOrigPoint
moveTo.Add(delta)
w.MoveTo(moveTo)
}
return nil
})

// Add the title bar to the supervisor.
s.Add(w.titleBar)
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)
}

// Children returns the window's child widgets.
func (w *Window) Children() []Widget {
return []Widget{
@@ -79,8 +182,18 @@ func (w *Window) Children() []Widget {
}
}

// 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() *Label {
func (w *Window) TitleBar() *Frame {
return w.titleBar
}

@@ -116,8 +229,3 @@ func (w *Window) Present(e render.Engine, P render.Point) {
// Call the BaseWidget Present in case we have subscribers.
w.BaseWidget.Present(e, P)
}

// Pack a widget into the window's frame.
func (w *Window) Pack(child Widget, config ...Pack) {
w.content.Pack(child, config...)
}

+ 148
- 0
window_manager.go View File

@@ -0,0 +1,148 @@
package ui

import (
"errors"
"fmt"

"git.kirsle.net/go/render"
)

/*
window_manager.go holds data types and Supervisor methods related to the
management of ui.Window widgets.
*/

// FocusedWindow is a doubly-linked list of recently focused Windows, with
// the current and most-recently focused on top. TODO make not exported.
type FocusedWindow struct {
window *Window
prev *FocusedWindow
next *FocusedWindow
}

// String of the FocusedWindow returns the underlying Window's String().
func (fw FocusedWindow) String() string {
return fw.window.String()
}

// Print the structure of the linked list from top to bottom.
func (fw *FocusedWindow) Print() {
var (
node = fw
i = 0
)
for node != nil {
fmt.Printf("[%d] window=%s prev=%s next=%s\n",
i, node.window, node.prev, node.next,
)
node = node.next
i++
}
}

// addWindow installs a Window into the supervisor to be managed. It is called
// by ui.Window.Supervise() and the newly added window becomes the focused
// one by default at the top of the linked list.
func (s *Supervisor) addWindow(win *Window) {
// Record in the window that it is managed by Supervisor, useful to control
// event propagation to non-focused windows.
win.managed = true

if s.winFocus == nil {
// First window added.
s.winFocus = &FocusedWindow{
window: win,
}
s.winTop = s.winFocus
s.winBottom = s.winFocus
win.SetFocus(true)
} else {
// New window, make it the top one.
oldTop := s.winFocus
s.winFocus = &FocusedWindow{
window: win,
next: oldTop,
}
oldTop.prev = s.winFocus
oldTop.window.SetFocus(false)
win.SetFocus(true)
}
}

// presentWindows draws the windows from bottom to top.
func (s *Supervisor) presentWindows(e render.Engine) {
item := s.winBottom
for item != nil {
item.window.Present(e, item.window.Point())
item = item.prev
}
}

// FocusWindow brings the given window to the top of the supervisor's focus.
//
// The window must have previously been added to the supervisor's Window Manager
// by calling the Supervise() method of the window.
func (s *Supervisor) FocusWindow(win *Window) error {
if s.winFocus == nil {
return errors.New("no windows managed by supervisor")
}

// If the top window is already the target, return.
if s.winFocus.window == win {
return nil
}

// Find the window in the linked list.
var (
item = s.winFocus // item as we iterate the list
oldTop = s.winFocus // original first item in the list
target *FocusedWindow // identified target window to raise
newBottom *FocusedWindow // if the target was the bottom, this is new bottom
i = 0
)
for item != nil {
if item.window == win {
// Found it!
target = item

// Is it the last window in the list? Record the new bottom node.
if item.next == nil && item.prev != nil {
newBottom = item.prev
}

// Remove it from its position in the linked list. Join its
// previous and next nodes to bridge the gap.
if item.next != nil {
item.next.prev = item.prev
}
if item.prev != nil {
item.prev.next = item.next
}

break
}
item = item.next
i++
}

// Found it?
if target != nil {
// Put the target at the top of the list, pointing to the old top.
target.next = oldTop
target.prev = nil
oldTop.prev = target
s.winFocus = target

// Fix the top and bottom pointers.
s.winTop = s.winFocus
if newBottom != nil {
s.winBottom = newBottom
}

// Toggle the focus states.
oldTop.window.SetFocus(false)
target.window.SetFocus(true)
}

return nil
}

Loading…
Cancel
Save