diff --git a/button.go b/button.go index 887cad8..6e52db2 100644 --- a/button.go +++ b/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 diff --git a/check_button.go b/check_button.go index e7960a1..2e00b99 100644 --- a/check_button.go +++ b/check_button.go @@ -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 }) } diff --git a/checkbox.go b/checkbox.go index cf25b6c..464af4c 100644 --- a/checkbox.go +++ b/checkbox.go @@ -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) } diff --git a/dragdrop.go b/dragdrop.go index ac1fea0..23bee9b 100644 --- a/dragdrop.go +++ b/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 } diff --git a/eg/windows/main.go b/eg/windows/main.go new file mode 100644 index 0000000..f083897 --- /dev/null +++ b/eg/windows/main.go @@ -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, + }) +} diff --git a/functions.go b/functions.go index c235acd..9613881 100644 --- a/functions.go +++ b/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 + } + } +} diff --git a/go.mod b/go.mod index fbd0de0..4627109 100644 --- a/go.mod +++ b/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 diff --git a/main_window.go b/main_window.go index 7a0663e..eed5ce5 100644 --- a/main_window.go +++ b/main_window.go @@ -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. diff --git a/menu.go b/menu.go index 08ef0e4..9aed60c 100644 --- a/menu.go +++ b/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 diff --git a/supervisor.go b/supervisor.go index dc64abb..fcdc525 100644 --- a/supervisor.go +++ b/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 - } - - // 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) - } + // 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 } - 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. diff --git a/tooltip.go b/tooltip.go index fa5db0d..5cee0d9 100644 --- a/tooltip.go +++ b/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 { diff --git a/widget.go b/widget.go index b5968fc..4fb560d 100644 --- a/widget.go +++ b/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) diff --git a/window.go b/window.go index 5d06b2d..5d6495a 100644 --- a/window.go +++ b/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 - 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...) -} diff --git a/window_manager.go b/window_manager.go new file mode 100644 index 0000000..644ed51 --- /dev/null +++ b/window_manager.go @@ -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 +}