Tooltips: how to draw on top of all widgets

By default, Tooltips will present after their associated widget presents
(if the mouse cursor is hovering over that widget, and the tooltip
should appear). But the tooltip is not guaranteed to draw "on top" of
neighboring doodads, unless you choose your Edge carefully depending on
the order you're drawing your widgets.

To solve this, Tooltips can be supervised to DrawOnTop() when they're
activated. To opt in, you simply call the Tooltip.Supervise() function
with your supervisor.
This commit is contained in:
Noah 2022-03-05 22:41:20 -08:00
parent 76ddda352d
commit c9c7b33647
3 changed files with 77 additions and 3 deletions

View File

@ -88,6 +88,9 @@ type Supervisor struct {
// List of window focus history for Window Manager.
winFocus *FocusedWindow
winBottom *FocusedWindow // pointer to bottom-most window
// Widgets that we should draw on top, such as Tooltips.
onTop []Widget
}
// WidgetSlot holds a widget with a unique ID number in a sorted list.
@ -103,6 +106,7 @@ func NewSupervisor() *Supervisor {
hovering: map[int]interface{}{},
clicked: map[int]bool{},
modals: []Widget{},
onTop: []Widget{},
dd: NewDragDrop(),
}
}
@ -493,6 +497,16 @@ func (s *Supervisor) Present(e render.Engine) {
modal.Present(e, modal.Point())
}
}
// Render any "on top" widgets like Tooltips.
if len(s.onTop) > 0 {
for _, widget := range s.onTop {
if widget.Hidden() {
continue
}
widget.Present(e, widget.Point())
}
}
}
// Add a widget to be supervised. Has no effect if the widget is already
@ -561,3 +575,24 @@ func (s *Supervisor) GetModal() Widget {
}
return s.modals[len(s.modals)-1]
}
/*
DrawOnTop gives the Supervisor a widget to manage the presentation of, for
example the Tooltip.
If you call Supervisor.Present() in your program's main loop, it will draw the
widgets that it manages, such as Windows, Menus and Tooltips. Call that function
last in your main loop, and these things are drawn on top of the rest of your
UI which you had called Present() on prior.
The current draw order of the Supervisor is as follows:
1. Managed windows are drawn in the order of most recently focused on top.
2. Pop-up modals such as Menus are drawn. Modals have an "event grab" and all
mouse events go to them, or clicking outside of them dismisses the modals.
3. DrawOnTop widgets such as Tooltips that should always be drawn "last" so as
not to be overwritten by neighboring widgets.
*/
func (s *Supervisor) DrawOnTop(w Widget) {
s.onTop = append(s.onTop, w)
}

View File

@ -12,7 +12,11 @@ func init() {
precomputeArrows()
}
// Tooltip attaches a mouse-over popup to another widget.
/*
Tooltip attaches a mouse-over popup to another widget.
*/
type Tooltip struct {
BaseWidget
@ -20,6 +24,7 @@ type Tooltip struct {
Text string // Text to show in the tooltip.
TextVariable *string // String pointer instead of text.
Edge Edge // side to display tooltip on
supervisor *Supervisor
style *style.Tooltip
target Widget
@ -68,7 +73,9 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
return nil
})
target.Handle(Present, func(ed EventData) error {
w.Present(ed.Engine, w.Point())
if w.supervisor == nil {
w.Present(ed.Engine, w.Point())
}
return nil
})
@ -81,6 +88,24 @@ func NewTooltip(target Widget, tt Tooltip) *Tooltip {
return w
}
/*
Supervise the tooltip widget. This will put the rendering of this widget under the
Supervisor's care to be drawn "on top" of all other widgets. Your main loop should
call the Supervisor.Present() function lastly so that things managed by it (such as
Windows, Menus and Tooltips) draw on top of everything else.
If you don't call this, the Tooltip by default will present when its attached widget
presents (if moused over and tooltip is to be visible). This alone is fine in many
simple use cases, but in a densely packed UI layout and depending on the Edge the
tooltip draws at, it may get over-drawn by other widgets and not appear "on top."
*/
func (w *Tooltip) Supervise(s *Supervisor) {
w.supervisor = s
// Supervisor will manage our presentation and draw us "on top"
w.supervisor.DrawOnTop(w)
}
// SetStyle sets the tooltip's default style.
func (w *Tooltip) SetStyle(v *style.Tooltip) {
if v == nil {

View File

@ -21,10 +21,24 @@ func ExampleTooltip() {
// Add a tooltip to it. The tooltip attaches itself to the button's
// MouseOver, MouseOut, Compute and Present handlers -- you don't need to
// place the tooltip inside the window or parent frame.
ui.NewTooltip(btn, ui.Tooltip{
tt := ui.NewTooltip(btn, ui.Tooltip{
Text: "This is a tooltip that pops up\non mouse hover!",
Edge: ui.Right,
})
// Notice: by default (with just the above code), the Tooltip will present
// when its target widget presents. For densely packed UIs, the Tooltip may
// be drawn "below" a neighboring widget, e.g. for horizontally packed buttons
// where the Tooltip is on the Right: the tooltip for the left-most button
// would present when the button does, but then the next button over will present
// and overwrite the tooltip.
//
// For many simple UIs you can arrange your widgets and tooltip edge to
// avoid this, but to guarantee the Tooltip always draws "on top", you
// need to give it your Supervisor so it can register itself into its
// Present stage (similar to window management). Be sure to call Supervisor.Present()
// lastly in your main loop.
tt.Supervise(mw.Supervisor())
mw.MainLoop()
}