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.
This commit is contained in:
parent
49f108f302
commit
7d9ba79cd2
12
button.go
12
button.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
17
dragdrop.go
17
dragdrop.go
|
@ -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
eg/windows/main.go
Normal file
94
eg/windows/main.go
Normal 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
functions.go
48
functions.go
|
@ -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
go.mod
2
go.mod
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
3
menu.go
3
menu.go
|
@ -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
|
||||
|
|
258
supervisor.go
258
supervisor.go
|
@ -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,68 +138,29 @@ 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
|
||||
}
|
||||
|
||||
for _, child := range hovering {
|
||||
var (
|
||||
id = child.id
|
||||
w = child.widget
|
||||
)
|
||||
if w.Hidden() {
|
||||
// TODO: somehow the Supervisor wasn't triggering hidden widgets
|
||||
// anyway, but I don't know why. Adding this check for safety.
|
||||
continue
|
||||
// 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
|
||||
}
|
||||
|
||||
// Cursor has intersected the widget.
|
||||
if _, ok := s.hovering[id]; !ok {
|
||||
w.Event(MouseOver, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
s.hovering[id] = nil
|
||||
}
|
||||
|
||||
_, isClicked := s.clicked[id]
|
||||
if ev.Button1 {
|
||||
if !isClicked {
|
||||
w.Event(MouseDown, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
s.clicked[id] = nil
|
||||
}
|
||||
} else if isClicked {
|
||||
w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
w.Event(Click, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
for _, child := range outside {
|
||||
var (
|
||||
id = child.id
|
||||
w = child.widget
|
||||
)
|
||||
|
||||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
w.Event(MouseOut, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
delete(s.hovering, id)
|
||||
}
|
||||
|
||||
if _, ok := s.clicked[id]; ok {
|
||||
w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
// Run events for the other widgets not in a managed window.
|
||||
s.runWidgetEvents(XY, ev, hovering, outside, false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -210,6 +196,143 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
|
|||
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
|
||||
)
|
||||
if w.Hidden() {
|
||||
// TODO: somehow the Supervisor wasn't triggering hidden widgets
|
||||
// anyway, but I don't know why. Adding this check for safety.
|
||||
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 {
|
||||
handle(w.Event(MouseOver, EventData{
|
||||
Point: XY,
|
||||
}))
|
||||
s.hovering[id] = nil
|
||||
}
|
||||
|
||||
isClicked, _ := s.clicked[id]
|
||||
if ev.Button1 {
|
||||
if !isClicked {
|
||||
err := w.Event(MouseDown, EventData{
|
||||
Point: XY,
|
||||
})
|
||||
handle(err)
|
||||
s.clicked[id] = true
|
||||
}
|
||||
} else if isClicked {
|
||||
handle(w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
}))
|
||||
handle(w.Event(Click, EventData{
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
for _, child := range outside {
|
||||
var (
|
||||
id = child.id
|
||||
w = child.widget
|
||||
)
|
||||
|
||||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
handle(w.Event(MouseOut, EventData{
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.hovering, id)
|
||||
}
|
||||
|
||||
if _, ok := s.clicked[id]; ok {
|
||||
handle(w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
|
||||
// If a stopPropagation was called, return it up the stack.
|
||||
if stopPropagation {
|
||||
return ranEvents, ErrStopPropagation
|
||||
}
|
||||
|
||||
// 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
|
||||
// they were added.
|
||||
func (s *Supervisor) Widgets() <-chan WidgetSlot {
|
||||
|
@ -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.
|
||||
|
|
12
tooltip.go
12
tooltip.go
|
@ -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 {
|
||||
|
|
19
widget.go
19
widget.go
|
@ -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)
|
||||
|
|
150
window.go
150
window.go
|
@ -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
|
||||
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
window_manager.go
Normal file
148
window_manager.go
Normal 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…
Reference in New Issue
Block a user