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.
This commit is contained in:
parent
0846fe22fc
commit
f9b305679a
|
@ -105,6 +105,9 @@ most complex.
|
||||||
* Pack() lets you add child widgets to the Frame, aligned against one side
|
* 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
|
or another, and ability to expand widgets to take up remaining space in
|
||||||
their part of the Frame.
|
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.
|
* [x] **Label**: Textual labels for your UI.
|
||||||
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
|
* Supports TrueType fonts, color, stroke, drop shadow, font size, etc.
|
||||||
* Variable binding support: TextVariable or IntVariable can point to a
|
* Variable binding support: TextVariable or IntVariable can point to a
|
||||||
|
|
12
button.go
12
button.go
|
@ -35,20 +35,20 @@ func NewButton(name string, child Widget) *Button {
|
||||||
Background: theme.ButtonBackgroundColor,
|
Background: theme.ButtonBackgroundColor,
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle(MouseOver, func(p render.Point) {
|
w.Handle(MouseOver, func(e EventData) {
|
||||||
w.hovering = true
|
w.hovering = true
|
||||||
w.SetBackground(theme.ButtonHoverColor)
|
w.SetBackground(theme.ButtonHoverColor)
|
||||||
})
|
})
|
||||||
w.Handle(MouseOut, func(p render.Point) {
|
w.Handle(MouseOut, func(e EventData) {
|
||||||
w.hovering = false
|
w.hovering = false
|
||||||
w.SetBackground(theme.ButtonBackgroundColor)
|
w.SetBackground(theme.ButtonBackgroundColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle(MouseDown, func(p render.Point) {
|
w.Handle(MouseDown, func(e EventData) {
|
||||||
w.clicked = true
|
w.clicked = true
|
||||||
w.SetBorderStyle(BorderSunken)
|
w.SetBorderStyle(BorderSunken)
|
||||||
})
|
})
|
||||||
w.Handle(MouseUp, func(p render.Point) {
|
w.Handle(MouseUp, func(e EventData) {
|
||||||
w.clicked = false
|
w.clicked = false
|
||||||
w.SetBorderStyle(BorderRaised)
|
w.SetBorderStyle(BorderRaised)
|
||||||
})
|
})
|
||||||
|
@ -74,6 +74,8 @@ func (w *Button) Compute(e render.Engine) {
|
||||||
H: size.H + w.BoxThickness(2),
|
H: size.H + w.BoxThickness(2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetText conveniently sets the button text, for Label children only.
|
// 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.
|
// Draw the text label inside.
|
||||||
w.child.Present(e, moveTo)
|
w.child.Present(e, moveTo)
|
||||||
|
|
||||||
|
w.BaseWidget.Present(e, P)
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,24 +78,24 @@ func (w *CheckButton) setup() {
|
||||||
Background: theme.ButtonBackgroundColor,
|
Background: theme.ButtonBackgroundColor,
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle(MouseOver, func(p render.Point) {
|
w.Handle(MouseOver, func(ed EventData) {
|
||||||
w.hovering = true
|
w.hovering = true
|
||||||
w.SetBackground(theme.ButtonHoverColor)
|
w.SetBackground(theme.ButtonHoverColor)
|
||||||
})
|
})
|
||||||
w.Handle(MouseOut, func(p render.Point) {
|
w.Handle(MouseOut, func(ed EventData) {
|
||||||
w.hovering = false
|
w.hovering = false
|
||||||
w.SetBackground(theme.ButtonBackgroundColor)
|
w.SetBackground(theme.ButtonBackgroundColor)
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle(MouseDown, func(p render.Point) {
|
w.Handle(MouseDown, func(ed EventData) {
|
||||||
w.clicked = true
|
w.clicked = true
|
||||||
w.SetBorderStyle(BorderSunken)
|
w.SetBorderStyle(BorderSunken)
|
||||||
})
|
})
|
||||||
w.Handle(MouseUp, func(p render.Point) {
|
w.Handle(MouseUp, func(ed EventData) {
|
||||||
w.clicked = false
|
w.clicked = false
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Handle(Click, func(p render.Point) {
|
w.Handle(Click, func(ed EventData) {
|
||||||
var sunken bool
|
var sunken bool
|
||||||
if w.BoolVar != nil {
|
if w.BoolVar != nil {
|
||||||
if *w.BoolVar {
|
if *w.BoolVar {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "git.kirsle.net/go/render"
|
|
||||||
|
|
||||||
// Checkbox combines a CheckButton with a widget like a Label.
|
// Checkbox combines a CheckButton with a widget like a Label.
|
||||||
type Checkbox struct {
|
type Checkbox struct {
|
||||||
Frame
|
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.
|
// Forward clicks on the child widget to the CheckButton.
|
||||||
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
|
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
|
||||||
func(e Event) {
|
func(e Event) {
|
||||||
w.child.Handle(e, func(p render.Point) {
|
w.child.Handle(e, func(ed EventData) {
|
||||||
w.button.Event(e, p)
|
w.button.Event(e, ed)
|
||||||
})
|
})
|
||||||
}(e)
|
}(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ func CreateButtons(window *ui.MainWindow, parent *ui.Frame) {
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// When clicked, change the window title to ID this button.
|
// 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)
|
window.SetTitle(parent.Name + ": " + setting.Label)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -40,16 +40,12 @@ func main() {
|
||||||
Padding: 4,
|
Padding: 4,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
button.Handle(ui.Click, func(p render.Point) {
|
button.Handle(ui.Click, func(ed ui.EventData) {
|
||||||
fmt.Println("I've been clicked!")
|
fmt.Println("I've been clicked!")
|
||||||
})
|
})
|
||||||
mw.Pack(button, ui.Pack{
|
mw.Pack(button, ui.Pack{
|
||||||
Side: ui.N,
|
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()
|
mw.MainLoop()
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ func main() {
|
||||||
btn := ui.NewButton(fmt.Sprintf("Button-%d", i), ui.NewLabel(ui.Label{
|
btn := ui.NewButton(fmt.Sprintf("Button-%d", i), ui.NewLabel(ui.Label{
|
||||||
Text: fmt.Sprintf("Button #%d", i),
|
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)
|
fmt.Printf("Button %d was clicked\n", i)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
148
eg/tooltip/main.go
Normal file
148
eg/tooltip/main.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
13
enums.go
Normal file
13
enums.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
// Edge name
|
||||||
|
type Edge int
|
||||||
|
|
||||||
|
// Edge values.
|
||||||
|
const (
|
||||||
|
Top Edge = iota
|
||||||
|
Left
|
||||||
|
Right
|
||||||
|
Bottom
|
||||||
|
FollowCursor
|
||||||
|
)
|
33
frame.go
33
frame.go
|
@ -1,6 +1,7 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/go/render"
|
"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.
|
// Children returns all of the child widgets.
|
||||||
func (w *Frame) Children() []Widget {
|
func (w *Frame) Children() []Widget {
|
||||||
return w.widgets
|
return w.widgets
|
||||||
|
@ -52,6 +71,9 @@ func (w *Frame) Children() []Widget {
|
||||||
func (w *Frame) Compute(e render.Engine) {
|
func (w *Frame) Compute(e render.Engine) {
|
||||||
w.computePacked(e)
|
w.computePacked(e)
|
||||||
w.computePlaced(e)
|
w.computePlaced(e)
|
||||||
|
|
||||||
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the Frame.
|
// 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.X+p.X+w.BoxThickness(1),
|
||||||
P.Y+p.Y+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)
|
child.Present(e, moveTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call the BaseWidget Present in case we have subscribers.
|
||||||
|
w.BaseWidget.Present(e, P)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ func (w *Frame) Pack(child Widget, config ...Pack) {
|
||||||
widget: child,
|
widget: child,
|
||||||
pack: C,
|
pack: C,
|
||||||
})
|
})
|
||||||
w.widgets = append(w.widgets, child)
|
w.Add(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
// computePacked processes all the Pack layout widgets in the Frame.
|
// computePacked processes all the Pack layout widgets in the Frame.
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (w *Frame) Place(child Widget, config Place) {
|
||||||
widget: child,
|
widget: child,
|
||||||
place: config,
|
place: config,
|
||||||
})
|
})
|
||||||
w.widgets = append(w.widgets, child)
|
w.Add(child)
|
||||||
|
|
||||||
// Adopt the child widget so it can access the Frame.
|
// Adopt the child widget so it can access the Frame.
|
||||||
child.SetParent(w)
|
child.SetParent(w)
|
||||||
|
@ -61,6 +61,7 @@ func (w *Frame) computePlaced(e render.Engine) {
|
||||||
switch row.place.Strategy() {
|
switch row.place.Strategy() {
|
||||||
case "Point":
|
case "Point":
|
||||||
row.widget.MoveTo(row.place.Point)
|
row.widget.MoveTo(row.place.Point)
|
||||||
|
row.widget.Compute(e)
|
||||||
case "Side":
|
case "Side":
|
||||||
var moveTo render.Point
|
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)
|
moveTo.Y = frameSize.H - (w.Size().H / 2) - (row.widget.Size().H / 2)
|
||||||
}
|
}
|
||||||
row.widget.MoveTo(moveTo)
|
row.widget.MoveTo(moveTo)
|
||||||
|
row.widget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this widget itself has placed widgets, call its function too.
|
// If this widget itself has placed widgets, call its function too.
|
||||||
|
|
6
image.go
6
image.go
|
@ -119,6 +119,9 @@ func (w *Image) GetRGBA() *image.RGBA {
|
||||||
// Compute the widget.
|
// Compute the widget.
|
||||||
func (w *Image) Compute(e render.Engine) {
|
func (w *Image) Compute(e render.Engine) {
|
||||||
w.Resize(w.texture.Size())
|
w.Resize(w.texture.Size())
|
||||||
|
|
||||||
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the widget.
|
// Present the widget.
|
||||||
|
@ -131,4 +134,7 @@ func (w *Image) Present(e render.Engine, p render.Point) {
|
||||||
H: size.H,
|
H: size.H,
|
||||||
}
|
}
|
||||||
e.Copy(w.texture, size, dst)
|
e.Copy(w.texture, size, dst)
|
||||||
|
|
||||||
|
// Call the BaseWidget Present in case we have subscribers.
|
||||||
|
w.BaseWidget.Present(e, p)
|
||||||
}
|
}
|
||||||
|
|
6
label.go
6
label.go
|
@ -101,6 +101,9 @@ func (w *Label) Compute(e render.Engine) {
|
||||||
H: maxRect.H + (padY * 2),
|
H: maxRect.H + (padY * 2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the label widget.
|
// 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),
|
Y: P.Y + border + padY + (i * w.lineHeight),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Call the BaseWidget Present in case we have subscribers.
|
||||||
|
w.BaseWidget.Present(e, P)
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,6 @@ func NewMainWindow(title string, dimensions ...int) (*MainWindow, error) {
|
||||||
// Add a default frame to the window.
|
// Add a default frame to the window.
|
||||||
mw.frame = NewFrame("MainWindow Body")
|
mw.frame = NewFrame("MainWindow Body")
|
||||||
mw.frame.SetBackground(render.RGBA(0, 153, 255, 100))
|
mw.frame.SetBackground(render.RGBA(0, 153, 255, 100))
|
||||||
mw.Add(mw.frame)
|
|
||||||
|
|
||||||
// Compute initial window size.
|
// Compute initial window size.
|
||||||
mw.resized()
|
mw.resized()
|
||||||
|
|
8
menu.go
8
menu.go
|
@ -36,11 +36,17 @@ func NewMenu(name string) *Menu {
|
||||||
// Compute the menu
|
// Compute the menu
|
||||||
func (w *Menu) Compute(e render.Engine) {
|
func (w *Menu) Compute(e render.Engine) {
|
||||||
w.body.Compute(e)
|
w.body.Compute(e)
|
||||||
|
|
||||||
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the menu
|
// Present the menu
|
||||||
func (w *Menu) Present(e render.Engine, p render.Point) {
|
func (w *Menu) Present(e render.Engine, p render.Point) {
|
||||||
w.body.Present(e, p)
|
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.
|
// AddItem quickly adds an item to a menu.
|
||||||
|
@ -90,7 +96,7 @@ func NewMenuItem(label string, command func()) *MenuItem {
|
||||||
Background: render.Blue,
|
Background: render.Blue,
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Button.Handle(Click, func(p render.Point) {
|
w.Button.Handle(Click, func(ed EventData) {
|
||||||
w.Command()
|
w.Command()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,19 @@ const (
|
||||||
KeyUp
|
KeyUp
|
||||||
KeyPress
|
KeyPress
|
||||||
Drop
|
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
|
// Supervisor keeps track of widgets of interest to notify them about
|
||||||
// interaction events such as mouse hovers and clicks in their general
|
// interaction events such as mouse hovers and clicks in their general
|
||||||
// vicinity.
|
// vicinity.
|
||||||
|
@ -97,7 +108,9 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
if !ev.Button1 && !ev.Button3 {
|
if !ev.Button1 && !ev.Button3 {
|
||||||
// The mouse has been released. TODO: make mouse button important?
|
// The mouse has been released. TODO: make mouse button important?
|
||||||
for _, child := range hovering {
|
for _, child := range hovering {
|
||||||
child.widget.Event(Drop, XY)
|
child.widget.Event(Drop, EventData{
|
||||||
|
Point: XY,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
s.DragStop()
|
s.DragStop()
|
||||||
}
|
}
|
||||||
|
@ -117,19 +130,27 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
|
|
||||||
// Cursor has intersected the widget.
|
// Cursor has intersected the widget.
|
||||||
if _, ok := s.hovering[id]; !ok {
|
if _, ok := s.hovering[id]; !ok {
|
||||||
w.Event(MouseOver, XY)
|
w.Event(MouseOver, EventData{
|
||||||
|
Point: XY,
|
||||||
|
})
|
||||||
s.hovering[id] = nil
|
s.hovering[id] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, isClicked := s.clicked[id]
|
_, isClicked := s.clicked[id]
|
||||||
if ev.Button1 {
|
if ev.Button1 {
|
||||||
if !isClicked {
|
if !isClicked {
|
||||||
w.Event(MouseDown, XY)
|
w.Event(MouseDown, EventData{
|
||||||
|
Point: XY,
|
||||||
|
})
|
||||||
s.clicked[id] = nil
|
s.clicked[id] = nil
|
||||||
}
|
}
|
||||||
} else if isClicked {
|
} else if isClicked {
|
||||||
w.Event(MouseUp, XY)
|
w.Event(MouseUp, EventData{
|
||||||
w.Event(Click, XY)
|
Point: XY,
|
||||||
|
})
|
||||||
|
w.Event(Click, EventData{
|
||||||
|
Point: XY,
|
||||||
|
})
|
||||||
delete(s.clicked, id)
|
delete(s.clicked, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,12 +162,16 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
||||||
|
|
||||||
// Cursor is not intersecting the widget.
|
// Cursor is not intersecting the widget.
|
||||||
if _, ok := s.hovering[id]; ok {
|
if _, ok := s.hovering[id]; ok {
|
||||||
w.Event(MouseOut, XY)
|
w.Event(MouseOut, EventData{
|
||||||
|
Point: XY,
|
||||||
|
})
|
||||||
delete(s.hovering, id)
|
delete(s.hovering, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := s.clicked[id]; ok {
|
if _, ok := s.clicked[id]; ok {
|
||||||
w.Event(MouseUp, XY)
|
w.Event(MouseUp, EventData{
|
||||||
|
Point: XY,
|
||||||
|
})
|
||||||
delete(s.clicked, id)
|
delete(s.clicked, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
294
tooltip.go
Normal file
294
tooltip.go
Normal file
|
@ -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 = "<empty>"
|
||||||
|
}
|
||||||
|
|
||||||
|
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)},
|
||||||
|
}
|
||||||
|
}
|
33
widget.go
33
widget.go
|
@ -32,8 +32,8 @@ type Widget interface {
|
||||||
ResizeAuto(render.Rect)
|
ResizeAuto(render.Rect)
|
||||||
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
||||||
|
|
||||||
Handle(Event, func(render.Point))
|
Handle(Event, func(EventData))
|
||||||
Event(Event, render.Point) // called internally to trigger an event
|
Event(Event, EventData) // called internally to trigger an event
|
||||||
|
|
||||||
// Thickness of the padding + border + outline.
|
// Thickness of the padding + border + outline.
|
||||||
BoxThickness(multiplier int) int
|
BoxThickness(multiplier int) int
|
||||||
|
@ -117,7 +117,7 @@ type BaseWidget struct {
|
||||||
borderSize int
|
borderSize int
|
||||||
outlineColor render.Color
|
outlineColor render.Color
|
||||||
outlineSize int
|
outlineSize int
|
||||||
handlers map[Event][]func(render.Point)
|
handlers map[Event][]func(EventData)
|
||||||
hasParent bool
|
hasParent bool
|
||||||
parent Widget
|
parent Widget
|
||||||
}
|
}
|
||||||
|
@ -471,23 +471,40 @@ func (w *BaseWidget) SetOutlineSize(v int) {
|
||||||
w.outlineSize = v
|
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.
|
// 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 {
|
if handlers, ok := w.handlers[event]; ok {
|
||||||
for _, fn := range handlers {
|
for _, fn := range handlers {
|
||||||
fn(p)
|
fn(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle an event in the widget.
|
// 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 {
|
if w.handlers == nil {
|
||||||
w.handlers = map[Event][]func(render.Point){}
|
w.handlers = map[Event][]func(EventData){}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := w.handlers[event]; !ok {
|
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)
|
w.handlers[event] = append(w.handlers[event], fn)
|
||||||
|
|
|
@ -104,11 +104,17 @@ func (w *Window) ConfigureTitle(C Config) {
|
||||||
// Compute the window.
|
// Compute the window.
|
||||||
func (w *Window) Compute(e render.Engine) {
|
func (w *Window) Compute(e render.Engine) {
|
||||||
w.body.Compute(e)
|
w.body.Compute(e)
|
||||||
|
|
||||||
|
// Call the BaseWidget Compute in case we have subscribers.
|
||||||
|
w.BaseWidget.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the window.
|
// Present the window.
|
||||||
func (w *Window) Present(e render.Engine, P render.Point) {
|
func (w *Window) Present(e render.Engine, P render.Point) {
|
||||||
w.body.Present(e, P)
|
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.
|
// Pack a widget into the window's frame.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user