From f9b305679afa3ea4523e25950d7a21351940dfc1 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 9 Mar 2020 17:13:33 -0700 Subject: [PATCH] Tooltip Widget and Event Refactor * Tooltip can be added to any target widget (e.g. Button) and pop up on mouse over. * Refactor the event system. Instead of passing a render.Point to all event handlers, pass an EventData struct which can hold the Point or the render.Engine. * Add event types Computed and Present, so a widget can set a handler on whenever its Computed or Present method is called. --- README.md | 3 + button.go | 12 +- check_button.go | 10 +- checkbox.go | 6 +- eg/frame-place/main.go | 2 +- eg/hello-world/main.go | 6 +- eg/main.go | 2 +- eg/tooltip/main.go | 148 +++++++++++++++++++++ enums.go | 13 ++ frame.go | 33 +++-- frame_pack.go | 2 +- frame_place.go | 4 +- image.go | 6 + label.go | 6 + main_window.go | 1 - menu.go | 8 +- supervisor.go | 39 +++++- tooltip.go | 294 +++++++++++++++++++++++++++++++++++++++++ widget.go | 33 +++-- window.go | 6 + 20 files changed, 587 insertions(+), 47 deletions(-) create mode 100644 eg/tooltip/main.go create mode 100644 enums.go create mode 100644 tooltip.go diff --git a/README.md b/README.md index 8ecac25..fb87bb5 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ most complex. * Pack() lets you add child widgets to the Frame, aligned against one side or another, and ability to expand widgets to take up remaining space in their part of the Frame. + * Place() lets you place child widgets relative to the parent. You can place + it at an exact Point, or against the Top, Left, Bottom or Right sides, or + aligned to the Center (horizontal) or Middle (vertical) of the parent. * [x] **Label**: Textual labels for your UI. * Supports TrueType fonts, color, stroke, drop shadow, font size, etc. * Variable binding support: TextVariable or IntVariable can point to a diff --git a/button.go b/button.go index 4eda12b..887cad8 100644 --- a/button.go +++ b/button.go @@ -35,20 +35,20 @@ func NewButton(name string, child Widget) *Button { Background: theme.ButtonBackgroundColor, }) - w.Handle(MouseOver, func(p render.Point) { + w.Handle(MouseOver, func(e EventData) { w.hovering = true w.SetBackground(theme.ButtonHoverColor) }) - w.Handle(MouseOut, func(p render.Point) { + w.Handle(MouseOut, func(e EventData) { w.hovering = false w.SetBackground(theme.ButtonBackgroundColor) }) - w.Handle(MouseDown, func(p render.Point) { + w.Handle(MouseDown, func(e EventData) { w.clicked = true w.SetBorderStyle(BorderSunken) }) - w.Handle(MouseUp, func(p render.Point) { + w.Handle(MouseUp, func(e EventData) { w.clicked = false w.SetBorderStyle(BorderRaised) }) @@ -74,6 +74,8 @@ func (w *Button) Compute(e render.Engine) { H: size.H + w.BoxThickness(2), }) } + + w.BaseWidget.Compute(e) } // SetText conveniently sets the button text, for Label children only. @@ -118,4 +120,6 @@ func (w *Button) Present(e render.Engine, P render.Point) { // Draw the text label inside. w.child.Present(e, moveTo) + + w.BaseWidget.Present(e, P) } diff --git a/check_button.go b/check_button.go index 84d5044..e7960a1 100644 --- a/check_button.go +++ b/check_button.go @@ -78,24 +78,24 @@ func (w *CheckButton) setup() { Background: theme.ButtonBackgroundColor, }) - w.Handle(MouseOver, func(p render.Point) { + w.Handle(MouseOver, func(ed EventData) { w.hovering = true w.SetBackground(theme.ButtonHoverColor) }) - w.Handle(MouseOut, func(p render.Point) { + w.Handle(MouseOut, func(ed EventData) { w.hovering = false w.SetBackground(theme.ButtonBackgroundColor) }) - w.Handle(MouseDown, func(p render.Point) { + w.Handle(MouseDown, func(ed EventData) { w.clicked = true w.SetBorderStyle(BorderSunken) }) - w.Handle(MouseUp, func(p render.Point) { + w.Handle(MouseUp, func(ed EventData) { w.clicked = false }) - w.Handle(Click, func(p render.Point) { + w.Handle(Click, func(ed EventData) { var sunken bool if w.BoolVar != nil { if *w.BoolVar { diff --git a/checkbox.go b/checkbox.go index bc8507b..cf25b6c 100644 --- a/checkbox.go +++ b/checkbox.go @@ -1,7 +1,5 @@ package ui -import "git.kirsle.net/go/render" - // Checkbox combines a CheckButton with a widget like a Label. type Checkbox struct { Frame @@ -37,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(p render.Point) { - w.button.Event(e, p) + w.child.Handle(e, func(ed EventData) { + w.button.Event(e, ed) }) }(e) } diff --git a/eg/frame-place/main.go b/eg/frame-place/main.go index 8cf63c6..ad1419d 100644 --- a/eg/frame-place/main.go +++ b/eg/frame-place/main.go @@ -134,7 +134,7 @@ func CreateButtons(window *ui.MainWindow, parent *ui.Frame) { })) // When clicked, change the window title to ID this button. - button.Handle(ui.Click, func(p render.Point) { + button.Handle(ui.Click, func(ed ui.EventData) { window.SetTitle(parent.Name + ": " + setting.Label) }) diff --git a/eg/hello-world/main.go b/eg/hello-world/main.go index 07f092a..32b5641 100644 --- a/eg/hello-world/main.go +++ b/eg/hello-world/main.go @@ -40,16 +40,12 @@ func main() { Padding: 4, }, })) - button.Handle(ui.Click, func(p render.Point) { + button.Handle(ui.Click, func(ed ui.EventData) { fmt.Println("I've been clicked!") }) mw.Pack(button, ui.Pack{ Side: ui.N, }) - // Add the button to the MainWindow's Supervisor so it can be - // clicked on and interacted with. - mw.Add(button) - mw.MainLoop() } diff --git a/eg/main.go b/eg/main.go index 84fa54e..01be29d 100644 --- a/eg/main.go +++ b/eg/main.go @@ -50,7 +50,7 @@ func main() { btn := ui.NewButton(fmt.Sprintf("Button-%d", i), ui.NewLabel(ui.Label{ Text: fmt.Sprintf("Button #%d", i), })) - btn.Handle(ui.Click, func(p render.Point) { + btn.Handle(ui.Click, func(ed ui.EventData) { fmt.Printf("Button %d was clicked\n", i) }) diff --git a/eg/tooltip/main.go b/eg/tooltip/main.go new file mode 100644 index 0000000..005a209 --- /dev/null +++ b/eg/tooltip/main.go @@ -0,0 +1,148 @@ +package main + +import ( + "git.kirsle.net/go/render" + "git.kirsle.net/go/render/sdl" + "git.kirsle.net/go/ui" +) + +func init() { + sdl.DefaultFontFilename = "../DejaVuSans.ttf" +} + +func main() { + mw, err := ui.NewMainWindow("Tooltip Demo", 800, 600) + if err != nil { + panic(err) + } + + mw.SetBackground(render.White) + CreateButtons(mw, mw.Frame()) + + btn := ui.NewButton("Test", ui.NewLabel(ui.Label{ + Text: "Click me", + Font: render.Text{ + Size: 32, + }, + })) + mw.Place(btn, ui.Place{ + Center: true, + Middle: true, + }) + + ui.NewTooltip(btn, ui.Tooltip{ + Text: "Hello world\nGoodbye mars!\nBlah blah blah...\nLOL", + Edge: ui.Right, + }) + + mw.MainLoop() +} + +// CreateButtons creates a set of Placed buttons around all the edges and +// center of the parent frame. +func CreateButtons(window *ui.MainWindow, parent *ui.Frame) { + // Draw buttons around the edges of the window. + buttons := []struct { + Label string + Edge ui.Edge + Place ui.Place + }{ + { + Label: "Top Left", + Edge: ui.Right, + Place: ui.Place{ + Point: render.NewPoint(12, 12), + }, + }, + { + Label: "Top Middle", + Edge: ui.Bottom, + Place: ui.Place{ + Top: 12, + Center: true, + }, + }, + { + Label: "Top Right", + Edge: ui.Left, + Place: ui.Place{ + Top: 12, + Right: 12, + }, + }, + { + Label: "Left Middle", + Edge: ui.Right, + Place: ui.Place{ + Left: 12, + Middle: true, + }, + }, + { + Label: "Center", + Edge: ui.Bottom, + Place: ui.Place{ + Center: true, + Middle: true, + }, + }, + { + Label: "Right Middle", + Edge: ui.Left, + Place: ui.Place{ + Right: 12, + Middle: true, + }, + }, + { + Label: "Bottom Left", + Edge: ui.Right, + Place: ui.Place{ + Left: 12, + Bottom: 12, + }, + }, + { + Label: "Bottom Center", + Edge: ui.Top, + Place: ui.Place{ + Bottom: 12, + Center: true, + }, + }, + { + Label: "Bottom Right", + Edge: ui.Left, + Place: ui.Place{ + Bottom: 12, + Right: 12, + }, + }, + } + for _, setting := range buttons { + setting := setting + + button := ui.NewButton(setting.Label, ui.NewLabel(ui.Label{ + Text: setting.Label, + Font: render.Text{ + FontFilename: "../DejaVuSans.ttf", + Size: 12, + Color: render.Black, + }, + })) + + // When clicked, change the window title to ID this button. + button.Handle(ui.Click, func(ed ui.EventData) { + window.SetTitle(parent.Name + ": " + setting.Label) + }) + + // Tooltip for it. + ui.NewTooltip(button, ui.Tooltip{ + Text: setting.Label + " Tooltip", + Edge: setting.Edge, + }) + + parent.Place(button, setting.Place) + window.Add(button) + } +} diff --git a/enums.go b/enums.go new file mode 100644 index 0000000..a367bd9 --- /dev/null +++ b/enums.go @@ -0,0 +1,13 @@ +package ui + +// Edge name +type Edge int + +// Edge values. +const ( + Top Edge = iota + Left + Right + Bottom + FollowCursor +) diff --git a/frame.go b/frame.go index 90bcbf5..03bc3c9 100644 --- a/frame.go +++ b/frame.go @@ -1,6 +1,7 @@ package ui import ( + "errors" "fmt" "git.kirsle.net/go/render" @@ -43,6 +44,24 @@ func (w *Frame) Setup() { } } +// Add a child widget to the frame. When the frame Presents itself, it also +// presents child widgets. This method is safe to call multiple times: it ensures +// the widget is not already a child of the Frame before adding it. +func (w *Frame) Add(child Widget) error { + if child == w { + return errors.New("can't add self to frame") + } + + // Ensure child is new to the frame. + for _, widget := range w.widgets { + if widget == child { + return errors.New("widget already added to frame") + } + } + w.widgets = append(w.widgets, child) + return nil +} + // Children returns all of the child widgets. func (w *Frame) Children() []Widget { return w.widgets @@ -52,6 +71,9 @@ func (w *Frame) Children() []Widget { func (w *Frame) Compute(e render.Engine) { w.computePacked(e) w.computePlaced(e) + + // Call the BaseWidget Compute in case we have subscribers. + w.BaseWidget.Compute(e) } // Present the Frame. @@ -83,14 +105,9 @@ func (w *Frame) Present(e render.Engine, P render.Point) { P.X+p.X+w.BoxThickness(1), P.Y+p.Y+w.BoxThickness(1), ) - // if child.ID() == "Canvas" { - // log.Debug("Frame X=%d Child X=%d Box=%d Point=%s", P.X, p.X, w.BoxThickness(1), p) - // log.Debug("Frame Y=%d Child Y=%d Box=%d MoveTo=%s", P.Y, p.Y, w.BoxThickness(1), moveTo) - // } - // child.MoveTo(moveTo) // TODO: if uncommented the child will creep down the parent each tick - // if child.ID() == "Canvas" { - // log.Debug("New Point: %s", child.Point()) - // } child.Present(e, moveTo) } + + // Call the BaseWidget Present in case we have subscribers. + w.BaseWidget.Present(e, P) } diff --git a/frame_pack.go b/frame_pack.go index 38c7767..8021bee 100644 --- a/frame_pack.go +++ b/frame_pack.go @@ -53,7 +53,7 @@ func (w *Frame) Pack(child Widget, config ...Pack) { widget: child, pack: C, }) - w.widgets = append(w.widgets, child) + w.Add(child) } // computePacked processes all the Pack layout widgets in the Frame. diff --git a/frame_place.go b/frame_place.go index 2b2c00d..e715358 100644 --- a/frame_place.go +++ b/frame_place.go @@ -43,7 +43,7 @@ func (w *Frame) Place(child Widget, config Place) { widget: child, place: config, }) - w.widgets = append(w.widgets, child) + w.Add(child) // Adopt the child widget so it can access the Frame. child.SetParent(w) @@ -61,6 +61,7 @@ func (w *Frame) computePlaced(e render.Engine) { switch row.place.Strategy() { case "Point": row.widget.MoveTo(row.place.Point) + row.widget.Compute(e) case "Side": var moveTo render.Point @@ -87,6 +88,7 @@ func (w *Frame) computePlaced(e render.Engine) { moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2) } row.widget.MoveTo(moveTo) + row.widget.Compute(e) } // If this widget itself has placed widgets, call its function too. diff --git a/image.go b/image.go index 4ad251b..69e331f 100644 --- a/image.go +++ b/image.go @@ -119,6 +119,9 @@ func (w *Image) GetRGBA() *image.RGBA { // Compute the widget. func (w *Image) Compute(e render.Engine) { w.Resize(w.texture.Size()) + + // Call the BaseWidget Compute in case we have subscribers. + w.BaseWidget.Compute(e) } // Present the widget. @@ -131,4 +134,7 @@ func (w *Image) Present(e render.Engine, p render.Point) { H: size.H, } e.Copy(w.texture, size, dst) + + // Call the BaseWidget Present in case we have subscribers. + w.BaseWidget.Present(e, p) } diff --git a/label.go b/label.go index a654116..cb3cee4 100644 --- a/label.go +++ b/label.go @@ -101,6 +101,9 @@ func (w *Label) Compute(e render.Engine) { H: maxRect.H + (padY * 2), }) } + + // Call the BaseWidget Compute in case we have subscribers. + w.BaseWidget.Compute(e) } // Present the label widget. @@ -125,4 +128,7 @@ func (w *Label) Present(e render.Engine, P render.Point) { Y: P.Y + border + padY + (i * w.lineHeight), }) } + + // Call the BaseWidget Present in case we have subscribers. + w.BaseWidget.Present(e, P) } diff --git a/main_window.go b/main_window.go index d90faa4..7a0663e 100644 --- a/main_window.go +++ b/main_window.go @@ -69,7 +69,6 @@ func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) { // Add a default frame to the window. mw.frame = NewFrame("MainWindow Body") mw.frame.SetBackground(render.RGBA(0, 153, 255, 100)) - mw.Add(mw.frame) // Compute initial window size. mw.resized() diff --git a/menu.go b/menu.go index a669ac6..08ef0e4 100644 --- a/menu.go +++ b/menu.go @@ -36,11 +36,17 @@ func NewMenu(name string) *Menu { // Compute the menu func (w *Menu) Compute(e render.Engine) { w.body.Compute(e) + + // Call the BaseWidget Compute in case we have subscribers. + w.BaseWidget.Compute(e) } // Present the menu func (w *Menu) Present(e render.Engine, p render.Point) { w.body.Present(e, p) + + // Call the BaseWidget Present in case we have subscribers. + w.BaseWidget.Present(e, p) } // AddItem quickly adds an item to a menu. @@ -90,7 +96,7 @@ func NewMenuItem(label string, command func()) *MenuItem { Background: render.Blue, }) - w.Button.Handle(Click, func(p render.Point) { + w.Button.Handle(Click, func(ed EventData) { w.Command() }) diff --git a/supervisor.go b/supervisor.go index 76b68ee..dc64abb 100644 --- a/supervisor.go +++ b/supervisor.go @@ -24,8 +24,19 @@ const ( KeyUp KeyPress Drop + Compute // fired whenever the widget runs Compute + Present // fired whenever the widget runs Present ) +// EventData carries common data to event handlers. +type EventData struct { + // Point is usually the cursor position on click and mouse events. + Point render.Point + + // Engine is the render engine on Compute and Present events. + Engine render.Engine +} + // Supervisor keeps track of widgets of interest to notify them about // interaction events such as mouse hovers and clicks in their general // vicinity. @@ -97,7 +108,9 @@ func (s *Supervisor) Loop(ev *event.State) error { if !ev.Button1 && !ev.Button3 { // The mouse has been released. TODO: make mouse button important? for _, child := range hovering { - child.widget.Event(Drop, XY) + child.widget.Event(Drop, EventData{ + Point: XY, + }) } s.DragStop() } @@ -117,19 +130,27 @@ func (s *Supervisor) Loop(ev *event.State) error { // Cursor has intersected the widget. if _, ok := s.hovering[id]; !ok { - w.Event(MouseOver, XY) + w.Event(MouseOver, EventData{ + Point: XY, + }) s.hovering[id] = nil } _, isClicked := s.clicked[id] if ev.Button1 { if !isClicked { - w.Event(MouseDown, XY) + w.Event(MouseDown, EventData{ + Point: XY, + }) s.clicked[id] = nil } } else if isClicked { - w.Event(MouseUp, XY) - w.Event(Click, XY) + w.Event(MouseUp, EventData{ + Point: XY, + }) + w.Event(Click, EventData{ + Point: XY, + }) delete(s.clicked, id) } } @@ -141,12 +162,16 @@ func (s *Supervisor) Loop(ev *event.State) error { // Cursor is not intersecting the widget. if _, ok := s.hovering[id]; ok { - w.Event(MouseOut, XY) + w.Event(MouseOut, EventData{ + Point: XY, + }) delete(s.hovering, id) } if _, ok := s.clicked[id]; ok { - w.Event(MouseUp, XY) + w.Event(MouseUp, EventData{ + Point: XY, + }) delete(s.clicked, id) } } diff --git a/tooltip.go b/tooltip.go new file mode 100644 index 0000000..5b59fa7 --- /dev/null +++ b/tooltip.go @@ -0,0 +1,294 @@ +package ui + +import ( + "fmt" + "strings" + + "git.kirsle.net/go/render" +) + +func init() { + precomputeArrows() +} + +// Tooltip attaches a mouse-over popup to another widget. +type Tooltip struct { + BaseWidget + + // Configurable attributes. + Text string // Text to show in the tooltip. + TextVariable *string // String pointer instead of text. + Edge Edge // side to display tooltip on + + target Widget + lineHeight int + font render.Text +} + +// Constants for tooltips. +const ( + tooltipArrowSize = 5 +) + +// NewTooltip creates a new tooltip attached to a widget. +func NewTooltip(target Widget, tt Tooltip) *Tooltip { + w := &Tooltip{ + Text: tt.Text, + TextVariable: tt.TextVariable, + Edge: tt.Edge, + target: target, + } + + // Default style. + w.Hide() + w.SetBackground(render.RGBA(0, 0, 0, 230)) + w.font = render.Text{ + Size: 10, + Color: render.White, + Padding: 4, + } + + // Add event bindings to the target widget. + // - Show the tooltip on MouseOver + // - 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) { + w.Show() + }) + target.Handle(MouseOut, func(ed EventData) { + w.Hide() + }) + target.Handle(Compute, func(ed EventData) { + w.Compute(ed.Engine) + }) + target.Handle(Present, func(ed EventData) { + w.Present(ed.Engine, w.Point()) + }) + + w.IDFunc(func() string { + return fmt.Sprintf(`Tooltip<"%s">`, w.Value()) + }) + + return w +} + +// Value returns the current text displayed in the tooltop, whether from the +// configured Text or the TextVariable pointer. +func (w *Tooltip) Value() string { + return w.text().Text +} + +// text returns the raw render.Text holding the current value to be displayed +// in the tooltip, either from Text or TextVariable. +func (w *Tooltip) text() render.Text { + if w.TextVariable != nil { + w.font.Text = *w.TextVariable + } else { + w.font.Text = w.Text + } + return w.font +} + +// Compute the size of the tooltip. +func (w *Tooltip) Compute(e render.Engine) { + // Compute the size based on the text. + w.computeText(e) + + // Compute the position based on the Edge and the target widget. + var ( + size = w.Size() + + target = w.target + tSize = target.Size() + tPoint = AbsolutePosition(target) + + moveTo render.Point + ) + + switch w.Edge { + case Top: + moveTo.Y = tPoint.Y - size.H - tooltipArrowSize + moveTo.X = tPoint.X + (tSize.W / 2) - (size.W / 2) + case Left: + moveTo.X = tPoint.X - size.W - tooltipArrowSize + moveTo.Y = tPoint.Y + (tSize.H / 2) - (size.H / 2) + case Right: + moveTo.X = tPoint.X + tSize.W + tooltipArrowSize + moveTo.Y = tPoint.Y + (tSize.H / 2) - (size.H / 2) + case Bottom: + moveTo.Y = tPoint.Y + tSize.H + tooltipArrowSize + moveTo.X = tPoint.X + (tSize.W / 2) - (size.W / 2) + } + + w.MoveTo(moveTo) +} + +// computeText handles the text compute, very similar to Label.Compute. +func (w *Tooltip) computeText(e render.Engine) { + text := w.text() + lines := strings.Split(text.Text, "\n") + + // Max rect to encompass all lines of text. + var maxRect = render.Rect{} + for _, line := range lines { + if line == "" { + line = "" + } + + text.Text = line // only this line at this time. + rect, err := e.ComputeTextRect(text) + if err != nil { + panic(fmt.Sprintf("%s: failed to compute text rect: %s", w, err)) // TODO return an error + } + + if rect.W > maxRect.W { + maxRect.W = rect.W + } + maxRect.H += rect.H + w.lineHeight = int(rect.H) + } + + var ( + padX = w.font.Padding + w.font.PadX + padY = w.font.Padding + w.font.PadY + ) + + w.Resize(render.Rect{ + W: maxRect.W + (padX * 2), + H: maxRect.H + (padY * 2), + }) +} + +// Present the tooltip. +func (w *Tooltip) Present(e render.Engine, P render.Point) { + if w.Hidden() { + return + } + + // Draw the text. + w.presentText(e, P) + + // Draw the arrow. + w.presentArrow(e, P) +} + +// presentText draws the text similar to Label. +func (w *Tooltip) presentText(e render.Engine, P render.Point) { + var ( + text = w.text() + padX = w.font.Padding + w.font.PadX + padY = w.font.Padding + w.font.PadY + ) + + w.DrawBox(e, P) + for i, line := range strings.Split(text.Text, "\n") { + text.Text = line + e.DrawText(text, render.Point{ + X: P.X + padX, + Y: P.Y + padY + (i * w.lineHeight), + }) + } +} + +// presentArrow draws the arrow between the tooltip and its target widget. +func (w *Tooltip) presentArrow(e render.Engine, P render.Point) { + var ( + // size = w.Size() + + target = w.target + tSize = target.Size() + tPoint = AbsolutePosition(target) + + drawAt render.Point + arrow [][]render.Point + ) + + switch w.Edge { + case Top: + arrow = arrowDown + drawAt = render.Point{ + X: tPoint.X + (tSize.W / 2) - tooltipArrowSize, + Y: tPoint.Y - tooltipArrowSize, + } + case Bottom: + arrow = arrowUp + drawAt = render.Point{ + X: tPoint.X + (tSize.W / 2) - tooltipArrowSize, + Y: tPoint.Y + tSize.H, + } + case Left: + arrow = arrowRight + drawAt = render.Point{ + X: tPoint.X - tooltipArrowSize, + Y: tPoint.Y + (tSize.H / 2) - tooltipArrowSize, + } + case Right: + arrow = arrowLeft + drawAt = render.Point{ + X: tPoint.X + tSize.W, + Y: tPoint.Y + (tSize.H / 2) - tooltipArrowSize, + } + } + drawArrow(e, w.Background(), drawAt, arrow) +} + +// Draw an arrow at a given top/left coordinate. +func drawArrow(e render.Engine, color render.Color, p render.Point, arrow [][]render.Point) { + for _, row := range arrow { + if len(row) == 1 { + point := render.NewPoint(row[0].X, row[0].Y) + point.Add(p) + e.DrawPoint(color, point) + } else { + start := render.NewPoint(row[0].X, row[0].Y) + end := render.NewPoint(row[1].X, row[1].Y) + start.Add(p) + end.Add(p) + e.DrawLine(color, start, end) + } + } +} + +// Arrows for the tooltip widget. +var ( + arrowDown [][]render.Point + arrowUp [][]render.Point + arrowLeft [][]render.Point + arrowRight [][]render.Point +) + +func precomputeArrows() { + arrowDown = [][]render.Point{ + {render.NewPoint(0, 0), render.NewPoint(10, 0)}, + {render.NewPoint(1, 1), render.NewPoint(9, 1)}, + {render.NewPoint(2, 2), render.NewPoint(8, 2)}, + {render.NewPoint(3, 3), render.NewPoint(7, 3)}, + {render.NewPoint(4, 4), render.NewPoint(6, 4)}, + {render.NewPoint(5, 5)}, + } + arrowUp = [][]render.Point{ + {render.NewPoint(5, 0)}, + {render.NewPoint(4, 1), render.NewPoint(6, 1)}, + {render.NewPoint(3, 2), render.NewPoint(7, 2)}, + {render.NewPoint(2, 3), render.NewPoint(8, 3)}, + {render.NewPoint(1, 4), render.NewPoint(9, 4)}, + // {render.NewPoint(0, 5), render.NewPoint(10, 5)}, + } + arrowLeft = [][]render.Point{ + {render.NewPoint(0, 5)}, + {render.NewPoint(1, 4), render.NewPoint(1, 6)}, + {render.NewPoint(2, 3), render.NewPoint(2, 7)}, + {render.NewPoint(3, 2), render.NewPoint(3, 8)}, + {render.NewPoint(4, 1), render.NewPoint(4, 9)}, + // {render.NewPoint(5, 0), render.NewPoint(5, 10)}, + } + arrowRight = [][]render.Point{ + {render.NewPoint(0, 0), render.NewPoint(0, 10)}, + {render.NewPoint(1, 1), render.NewPoint(1, 9)}, + {render.NewPoint(2, 2), render.NewPoint(2, 8)}, + {render.NewPoint(3, 3), render.NewPoint(3, 7)}, + {render.NewPoint(4, 4), render.NewPoint(4, 6)}, + {render.NewPoint(5, 5)}, + } +} diff --git a/widget.go b/widget.go index 97681d7..b5968fc 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(render.Point)) - Event(Event, render.Point) // called internally to trigger an event + Handle(Event, func(EventData)) + Event(Event, EventData) // 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(render.Point) + handlers map[Event][]func(EventData) hasParent bool parent Widget } @@ -471,23 +471,40 @@ func (w *BaseWidget) SetOutlineSize(v int) { w.outlineSize = v } +// Compute calls the base widget's Compute function, which just triggers +// events on widgets that want to be notified when the widget computes. +func (w *BaseWidget) Compute(e render.Engine) { + w.Event(Compute, EventData{ + Engine: e, + }) +} + +// Present calls the base widget's Present function, which just triggers +// events on widgets that want to be notified when the widget presents. +func (w *BaseWidget) Present(e render.Engine, p render.Point) { + w.Event(Present, EventData{ + Point: p, + Engine: e, + }) +} + // Event is called internally by Doodle to trigger an event. -func (w *BaseWidget) Event(event Event, p render.Point) { +func (w *BaseWidget) Event(event Event, e EventData) { if handlers, ok := w.handlers[event]; ok { for _, fn := range handlers { - fn(p) + fn(e) } } } // Handle an event in the widget. -func (w *BaseWidget) Handle(event Event, fn func(render.Point)) { +func (w *BaseWidget) Handle(event Event, fn func(EventData)) { if w.handlers == nil { - w.handlers = map[Event][]func(render.Point){} + w.handlers = map[Event][]func(EventData){} } if _, ok := w.handlers[event]; !ok { - w.handlers[event] = []func(render.Point){} + w.handlers[event] = []func(EventData){} } w.handlers[event] = append(w.handlers[event], fn) diff --git a/window.go b/window.go index c57d913..5d06b2d 100644 --- a/window.go +++ b/window.go @@ -104,11 +104,17 @@ func (w *Window) ConfigureTitle(C Config) { // Compute the window. func (w *Window) Compute(e render.Engine) { w.body.Compute(e) + + // Call the BaseWidget Compute in case we have subscribers. + w.BaseWidget.Compute(e) } // Present the window. func (w *Window) Present(e render.Engine, P render.Point) { w.body.Present(e, P) + + // Call the BaseWidget Present in case we have subscribers. + w.BaseWidget.Present(e, P) } // Pack a widget into the window's frame.