New widget: ColorPicker, plus other changes
New properties are added to EventData for Supervisor events: * Widget: a reference to the widget which is receiving the event. * Clicked (bool): for MouseMove events records if the primary button is pressed. * func RelativePoint(): returns a version of EventData.Point adjusted to be relative to the Widget (0,0 at the Widget's absolute position on screen). Other changes: * Destroy() method for the Widget interface: widgets that need to free up resources on teardown should define this, the BaseWidget provides a no-op implementation. * Window.Resize() will properly resize a Window. * Window.Center(w, h int) to easily center a window on screen.
This commit is contained in:
parent
0a6054baa6
commit
fee6e1e105
|
@ -154,6 +154,13 @@ most complex.
|
|||
* [x] **SelectBox**: a kind of MenuButton that lets the user choose a
|
||||
value from a list of possible values.
|
||||
|
||||
Some useful helper widgets:
|
||||
|
||||
* **ColorPicker**: a ui.Window popup that lets the user choose a color value.
|
||||
It shows a graphical gradient they can click on and an ability to enter a
|
||||
custom hexadecimal value by hand (needs assistance from your program).
|
||||
[Example](eg/colorpicker)
|
||||
|
||||
**Work in progress widgets:**
|
||||
|
||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||
|
|
585
colorpicker.go
Normal file
585
colorpicker.go
Normal file
|
@ -0,0 +1,585 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
)
|
||||
|
||||
// ColorPicker is a Window that allows the user to pick out a color.
|
||||
type ColorPicker struct {
|
||||
*Window
|
||||
|
||||
// Config settings.
|
||||
Title string
|
||||
Color render.Color // initial color selection
|
||||
Supervisor *Supervisor
|
||||
Engine render.Engine
|
||||
|
||||
// Callback function in case the user wants to manually enter a hex color code.
|
||||
// Your program should prompt them by any means and return their chosen color.
|
||||
// Return a zero color (render.Invisible) to mean cancel.
|
||||
OnManualInput func(callback func(render.Color))
|
||||
|
||||
then func(render.Color) // .Then() callback
|
||||
cancel func() // .OnCancel() callback
|
||||
selected render.Color
|
||||
|
||||
// SDL2 etc. structures that will need freed.
|
||||
tex render.Texturer
|
||||
}
|
||||
|
||||
// DefaultColorPickerSize sets the default window size used in NewColorPicker
|
||||
// unless the user specified overrides.
|
||||
var DefaultColorPickerSize = render.Rect{W: 240, H: 190}
|
||||
|
||||
// NewColorPicker creates a new ColorPicker window. Specify the dimensions
|
||||
// you want the window to appear in (width, height int) to specify a desired
|
||||
// window size or else the default will be DefaultColorPickerSize.
|
||||
func NewColorPicker(config ColorPicker, dimensions ...int) (*ColorPicker, error) {
|
||||
var size = DefaultColorPickerSize
|
||||
if len(dimensions) == 2 {
|
||||
size = render.NewRect(dimensions[0], dimensions[1])
|
||||
}
|
||||
|
||||
// Validate settings.
|
||||
if config.Title == "" {
|
||||
config.Title = "Select a color"
|
||||
}
|
||||
if config.Supervisor == nil {
|
||||
return nil, errors.New("a ui.Supervisor is required")
|
||||
}
|
||||
if config.Engine == nil {
|
||||
return nil, errors.New("a render.Engine is required")
|
||||
}
|
||||
|
||||
// Create the colorpicker window.
|
||||
window := NewWindow(config.Title)
|
||||
window.Resize(size)
|
||||
window.SetButtons(CloseButton)
|
||||
|
||||
w := &ColorPicker{
|
||||
Title: config.Title,
|
||||
Window: window,
|
||||
Supervisor: config.Supervisor,
|
||||
Engine: config.Engine,
|
||||
Color: config.Color,
|
||||
OnManualInput: config.OnManualInput,
|
||||
}
|
||||
|
||||
window.Handle(CloseWindow, func(ed EventData) error {
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := w.setup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.Window.Supervise(w.Supervisor)
|
||||
w.Window.Hide()
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// Then is a callback function when the user has chosen a color.
|
||||
func (w *ColorPicker) Then(callback func(render.Color)) {
|
||||
w.then = callback
|
||||
}
|
||||
|
||||
// OnCancel is a callback to handle the ColorPicker being dismissed by the user.
|
||||
func (w *ColorPicker) OnCancel(callback func()) {
|
||||
w.cancel = callback
|
||||
}
|
||||
|
||||
// setup the main UI of the ColorPicker window.
|
||||
func (w *ColorPicker) setup() error {
|
||||
// Load the color gradient image. Guaranteed not to error.
|
||||
if tex, err := w.Engine.StoreTexture("ui.ColorPicker/spectrum.png", MakeColorPickerGradient()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
w.tex = tex
|
||||
}
|
||||
|
||||
// Prepare values for the currently selected color.
|
||||
var (
|
||||
OriginalColor = w.Color
|
||||
CurrentColor = w.Color
|
||||
)
|
||||
if OriginalColor.IsZero() {
|
||||
OriginalColor = render.Black
|
||||
}
|
||||
CurrentColor = OriginalColor
|
||||
w.selected = CurrentColor
|
||||
|
||||
// Divide up the main frames.
|
||||
var (
|
||||
btnFrame = NewFrame("Buttons")
|
||||
frame = NewFrame("Main Frame")
|
||||
leftFrame = NewFrame("Content Frame")
|
||||
)
|
||||
|
||||
// DEBUG
|
||||
// btnFrame.SetBackground(render.Yellow)
|
||||
// frame.SetBackground(render.Red)
|
||||
// leftFrame.SetBackground(render.Green)
|
||||
|
||||
// The main frame vs. the button frame on bottom.
|
||||
w.Pack(frame, Pack{
|
||||
Side: N,
|
||||
Fill: true,
|
||||
Expand: true,
|
||||
})
|
||||
w.Pack(btnFrame, Pack{
|
||||
Side: N,
|
||||
PadY: 4,
|
||||
})
|
||||
|
||||
// The left and right frames of the main frame.
|
||||
frame.Pack(leftFrame, Pack{
|
||||
Side: N,
|
||||
Fill: true,
|
||||
Expand: true,
|
||||
})
|
||||
|
||||
////////
|
||||
// Buttons frame.
|
||||
{
|
||||
for _, config := range []struct {
|
||||
label string
|
||||
callback func()
|
||||
}{
|
||||
{"Ok", func() {
|
||||
w.Destroy()
|
||||
if w.then != nil {
|
||||
w.then(w.selected)
|
||||
}
|
||||
}},
|
||||
{"Cancel", func() {
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
w.Destroy()
|
||||
}},
|
||||
} {
|
||||
config := config
|
||||
|
||||
btn := NewButton(config.label, NewLabel(Label{
|
||||
Text: config.label,
|
||||
Font: DefaultFont.Update(render.Text{
|
||||
PadX: 8,
|
||||
PadY: 2,
|
||||
}),
|
||||
}))
|
||||
btn.Handle(Click, func(ed EventData) error {
|
||||
config.callback()
|
||||
return nil
|
||||
})
|
||||
w.Supervisor.Add(btn)
|
||||
btnFrame.Pack(btn, Pack{
|
||||
Side: W,
|
||||
PadX: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/////////
|
||||
// Left frame.
|
||||
{
|
||||
// The gradient image up top.
|
||||
gradient, _ := ImageFromImage(MakeColorPickerGradient())
|
||||
leftFrame.Pack(gradient, Pack{
|
||||
Side: N,
|
||||
PadX: 2,
|
||||
PadY: 4,
|
||||
})
|
||||
|
||||
// Below that: your Current Color | Original Color indicators.
|
||||
var (
|
||||
previewRow = NewFrame("Color Preview")
|
||||
curColorFrame = NewFrame("Current Color")
|
||||
origColorFrame = NewFrame("Original Color")
|
||||
previewDividerFrame = NewFrame("Divider")
|
||||
hexLabel = NewLabel(Label{
|
||||
Text: " Hex color:",
|
||||
Font: DefaultFont,
|
||||
})
|
||||
hexButton = NewButton("Hex Button", NewLabel(Label{
|
||||
Text: w.selected.ToHex(),
|
||||
Font: DefaultFont.Update(render.Text{
|
||||
PadX: 6,
|
||||
}),
|
||||
}))
|
||||
)
|
||||
curColorFrame.Configure(Config{
|
||||
Width: 24,
|
||||
Height: 24,
|
||||
Background: CurrentColor,
|
||||
BorderStyle: BorderSunken,
|
||||
BorderSize: 2,
|
||||
BorderColor: CurrentColor,
|
||||
})
|
||||
origColorFrame.Configure(Config{
|
||||
Width: 24,
|
||||
Height: 24,
|
||||
Background: OriginalColor,
|
||||
BorderColor: OriginalColor,
|
||||
BorderSize: 2,
|
||||
BorderStyle: BorderSunken,
|
||||
})
|
||||
previewDividerFrame.Configure(Config{
|
||||
Width: 1,
|
||||
Height: 24,
|
||||
BorderStyle: BorderRaised,
|
||||
BorderSize: 2,
|
||||
BorderColor: render.Grey,
|
||||
})
|
||||
previewRow.Pack(curColorFrame, Pack{
|
||||
Side: W,
|
||||
PadX: 4,
|
||||
})
|
||||
previewRow.Pack(previewDividerFrame, Pack{
|
||||
Side: W,
|
||||
})
|
||||
previewRow.Pack(origColorFrame, Pack{
|
||||
Side: W,
|
||||
PadX: 4,
|
||||
})
|
||||
previewRow.Pack(hexLabel, Pack{
|
||||
Side: W,
|
||||
})
|
||||
previewRow.Pack(hexButton, Pack{
|
||||
Side: W,
|
||||
PadX: 4,
|
||||
})
|
||||
leftFrame.Pack(previewRow, Pack{
|
||||
Side: N,
|
||||
FillX: true,
|
||||
PadX: 5,
|
||||
})
|
||||
|
||||
// Bind clicks into the spectrum image to update the selected color.
|
||||
gradient.Handle(MouseMove, func(ed EventData) error {
|
||||
if ed.Clicked {
|
||||
point := ed.RelativePoint()
|
||||
|
||||
color := render.FromColor(gradient.Image.At(point.X, point.Y))
|
||||
|
||||
w.selected = color
|
||||
hexButton.SetText(color.ToHex())
|
||||
curColorFrame.Configure(Config{
|
||||
Background: color,
|
||||
BorderColor: color,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Clicking the original color resets it.
|
||||
origColorFrame.Handle(Click, func(ed EventData) error {
|
||||
color := OriginalColor
|
||||
w.selected = color
|
||||
hexButton.SetText(color.ToHex())
|
||||
curColorFrame.Configure(Config{
|
||||
Background: color,
|
||||
BorderColor: color,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
// Clicking the Hex Button prompts the user to enter a hex code themselves.
|
||||
hexButton.Handle(Click, func(ed EventData) error {
|
||||
if w.OnManualInput != nil {
|
||||
w.OnManualInput(func(color render.Color) {
|
||||
if color.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
w.selected = color
|
||||
hexButton.SetText(color.ToHex())
|
||||
curColorFrame.Configure(Config{
|
||||
Background: color,
|
||||
BorderColor: color,
|
||||
})
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
w.Supervisor.Add(gradient)
|
||||
w.Supervisor.Add(origColorFrame)
|
||||
w.Supervisor.Add(hexButton)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compute the widget.
|
||||
func (w *ColorPicker) Compute(e render.Engine) {
|
||||
// TODO: free the w.tex
|
||||
}
|
||||
|
||||
// Destroy the ColorPicker widget. Call this instead of Hide() if you close the
|
||||
// widget programmatically! It will free up SDL textures and so on.
|
||||
func (w *ColorPicker) Destroy() {
|
||||
w.Hide()
|
||||
}
|
||||
|
||||
// ColorPickerPreset shows the preset color buttons.
|
||||
// Suggested to have 18 colors which is displayed in 2 rows of 9 buttons.
|
||||
// More presets may need larger than default window size to fit.
|
||||
var ColorPickerPreset = []string{
|
||||
// Row 1
|
||||
"#FFFFFF", // White
|
||||
"#CCCCCC", // Light grey
|
||||
"#FF0000", // Red
|
||||
"#FF9900", // Orange
|
||||
"#FFFF00", // Yellow
|
||||
"#00FF00", // Lime green
|
||||
"#00FFFF", // Cyan
|
||||
"#FF00FF", // Magenta
|
||||
"#FF9999", // Pastel red
|
||||
"#99FF99", // Pastel green
|
||||
|
||||
// Row 2
|
||||
"#000000", // Black
|
||||
"#999999", // Dark grey
|
||||
"#990000", // Dark red
|
||||
"#996600", // Brown
|
||||
"#999900", // Gold
|
||||
"#009900", // Green
|
||||
"#009999", // Teal
|
||||
"#000099", // Dark Blue
|
||||
"#990099", // Purple
|
||||
"#9999FF", // Pastel blue
|
||||
"#FFFF99", // Pastel yellow
|
||||
}
|
||||
|
||||
// Load the color spectrum image.
|
||||
func MakeColorPickerGradient() image.Image {
|
||||
data, err := base64.StdEncoding.DecodeString(colorPickerGradient)
|
||||
if err != nil {
|
||||
fmt.Printf("ui.MakeColorPickerGradient: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// debug
|
||||
ioutil.WriteFile("gradient.png", data, 0644)
|
||||
|
||||
image, err := png.Decode(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
fmt.Printf("ui.MakeColorPickerGradient: png.Decode: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
// colorPickerGradient holds Base64 PNG image data for the color picker graphic.
|
||||
const colorPickerGradient = `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
|
||||
aiCrcAAAAABJRU5ErkJggg==`
|
|
@ -15,4 +15,5 @@ screenshot and description:
|
|||
* [Themes](themes/): a UI demo that shows off the Default, Flat, and Dark UI
|
||||
themes as part of experimental theming support.
|
||||
* [TabFrame](tabframe/): demo for the TabFrame widget showing multiple Windows
|
||||
with tabbed interfaces.
|
||||
with tabbed interfaces.
|
||||
* [ColorPicker](colorpicker/): demo for the ColorPicker widget.
|
||||
|
|
20
eg/colorpicker/README.md
Normal file
20
eg/colorpicker/README.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
# ColorPicker Demo
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
||||
This demo shows off the ColorPicker window.
|
||||
|
||||
The ColorPicker lets you ask the user to select a color, visually,
|
||||
using a callback interface. While the UI toolkit doesn't support
|
||||
text input entry, there is a work-around to prompt the user to enter
|
||||
a color by hex code with assistance of your program.
|
||||
|
||||
In this example program, clicking on the Hex color button will prompt
|
||||
you via STDIN (check the terminal window!) to enter a color value. The
|
||||
UI will be frozen until your answer is given. For programs that don't
|
||||
need this, the Hex color button does nothing when clicked.
|
||||
|
||||
## Running It
|
||||
|
||||
From your terminal, just type `go run main.go` from this
|
||||
example's directory.
|
95
eg/colorpicker/main.go
Normal file
95
eg/colorpicker/main.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/sdl"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
var WindowColor = render.SkyBlue
|
||||
|
||||
func init() {
|
||||
sdl.DefaultFontFilename = "../DejaVuSans.ttf"
|
||||
}
|
||||
|
||||
func main() {
|
||||
mw, err := ui.NewMainWindow("ColorPicker Demo", 812, 375)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mw.SetBackground(WindowColor)
|
||||
|
||||
btn := ui.NewButton("Test", ui.NewLabel(ui.Label{
|
||||
Text: "Pick the background color",
|
||||
Font: render.Text{
|
||||
Size: 32,
|
||||
},
|
||||
}))
|
||||
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
colorpicker, err := ui.NewColorPicker(ui.ColorPicker{
|
||||
Title: "Select a background color",
|
||||
Supervisor: mw.Supervisor(),
|
||||
Engine: mw.Engine,
|
||||
Color: WindowColor,
|
||||
|
||||
// Until the UI toolkit has normal text entry controls, this work-around
|
||||
// allows your application to ask the user to enter a hex color code
|
||||
// themselves, using any means available. This is an asynchronous
|
||||
// procedure where you are given a callback function to send your answer
|
||||
// whenever you have it. For this example, look for the prompt
|
||||
// question in your terminal window!
|
||||
OnManualInput: func(callback func(render.Color)) {
|
||||
// Prompt the user to enter a hex color in the terminal.
|
||||
var s string
|
||||
fmt.Fprintf(os.Stderr, "Enter a hexadecimal color code> ")
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
for {
|
||||
s, _ = r.ReadString('\n')
|
||||
if s != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse it as a color.
|
||||
fmt.Printf("Answer: %s\n", s)
|
||||
color, err := render.HexColor(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Ping the callback function with our answer.
|
||||
callback(color)
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing colorpicker: %s\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
colorpicker.Center(mw.Engine.WindowSize())
|
||||
colorpicker.Then(func(color render.Color) {
|
||||
WindowColor = color
|
||||
mw.SetBackground(WindowColor)
|
||||
})
|
||||
colorpicker.OnCancel(func() {
|
||||
fmt.Println("ColorPicker was dismissed by user")
|
||||
})
|
||||
|
||||
fmt.Printf("Open ColorPicker: %+v\n", colorpicker)
|
||||
colorpicker.Show()
|
||||
return nil
|
||||
})
|
||||
mw.Place(btn, ui.Place{
|
||||
Center: true,
|
||||
Middle: true,
|
||||
})
|
||||
|
||||
mw.MainLoop()
|
||||
}
|
BIN
eg/colorpicker/screenshot.png
Normal file
BIN
eg/colorpicker/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
4
go.mod
4
go.mod
|
@ -3,7 +3,7 @@ module git.kirsle.net/go/ui
|
|||
go 1.16
|
||||
|
||||
require (
|
||||
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782
|
||||
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da
|
||||
github.com/veandco/go-sdl2 v0.4.8 // indirect
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
|
||||
)
|
||||
|
|
4
go.sum
4
go.sum
|
@ -2,6 +2,8 @@ git.kirsle.net/go/render v0.0.0-20210104010442-b4a1979a8ba1 h1:wGQLjBnWvqx7rU43y
|
|||
git.kirsle.net/go/render v0.0.0-20210104010442-b4a1979a8ba1/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
|
||||
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782 h1:Ko+NvZxmJbW+M1dA2jCSnV6qSpkLoZASramE6ltlf/s=
|
||||
git.kirsle.net/go/render v0.0.0-20210614025954-d77f5056b782/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
|
||||
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da h1:wbeh/hHiwmXqf/3VPrbE/PADTcT1niQWhxxK81Ize3o=
|
||||
git.kirsle.net/go/render v0.0.0-20211231003948-9e640ab5c3da/go.mod h1:ss7pvZbGWrMaDuZwyUTjV9+T0AJwAkxZZHwMFsvHrkk=
|
||||
github.com/veandco/go-sdl2 v0.4.1/go.mod h1:FB+kTpX9YTE+urhYiClnRzpOXbiWgaU3+5F2AB78DPg=
|
||||
github.com/veandco/go-sdl2 v0.4.7 h1:VfpCM+LfEGDbHdByglCo2bcBsevjFvzl8W0f6VLNitg=
|
||||
github.com/veandco/go-sdl2 v0.4.7/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
|
@ -12,6 +14,8 @@ golang.org/x/image v0.0.0-20210504121937-7319ad40d33e h1:PzJMNfFQx+QO9hrC1GwZ4Bo
|
|||
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
|
|
@ -53,6 +53,22 @@ type EventData struct {
|
|||
|
||||
// Supervisor is the reference to the supervisor who sent the event.
|
||||
Supervisor *Supervisor
|
||||
|
||||
// Widget is a reference to the widget receiving the event.
|
||||
Widget Widget
|
||||
|
||||
// Clicked is true if the primary mouse button is down during
|
||||
// a MouseMove
|
||||
Clicked bool
|
||||
}
|
||||
|
||||
// RelativePoint returns the ed.Point adjusted to be relative to the widget on screen.
|
||||
func (ed EventData) RelativePoint() render.Point {
|
||||
if ed.Widget == nil {
|
||||
return render.NewPoint(-1, -1)
|
||||
}
|
||||
abs := AbsolutePosition(ed.Widget)
|
||||
return render.NewPoint(ed.Point.X-abs.X, ed.Point.Y-abs.Y)
|
||||
}
|
||||
|
||||
// Supervisor keeps track of widgets of interest to notify them about
|
||||
|
@ -151,7 +167,8 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
|||
// The mouse has been released. TODO: make mouse button important?
|
||||
for _, child := range hovering {
|
||||
child.widget.Event(Drop, EventData{
|
||||
Point: XY,
|
||||
Widget: child.widget,
|
||||
Point: XY,
|
||||
})
|
||||
}
|
||||
s.DragStop()
|
||||
|
@ -159,7 +176,8 @@ func (s *Supervisor) Loop(ev *event.State) error {
|
|||
// 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,
|
||||
Widget: target,
|
||||
Point: XY,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -347,35 +365,42 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
|||
// Cursor has intersected the widget.
|
||||
if _, ok := s.hovering[id]; !ok {
|
||||
handle(w.Event(MouseOver, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
s.hovering[id] = nil
|
||||
}
|
||||
|
||||
// Mouse movement. NOTE: it is intentional that this fires on
|
||||
// every tick even if XY was the same as last time.
|
||||
handle(w.Event(MouseMove, EventData{
|
||||
Point: XY,
|
||||
}))
|
||||
|
||||
isClicked, _ := s.clicked[id]
|
||||
isClicked := s.clicked[id]
|
||||
if ev.Button1 {
|
||||
if !isClicked {
|
||||
err := w.Event(MouseDown, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
})
|
||||
handle(err)
|
||||
s.clicked[id] = true
|
||||
}
|
||||
} else if isClicked {
|
||||
handle(w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
handle(w.Event(Click, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
|
||||
// Mouse movement. NOTE: it is intentional that this fires on
|
||||
// every tick even if XY was the same as last time.
|
||||
handle(w.Event(MouseMove, EventData{
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
Clicked: ev.Button1,
|
||||
}))
|
||||
|
||||
}
|
||||
for _, child := range outside {
|
||||
var (
|
||||
|
@ -394,14 +419,16 @@ func (s *Supervisor) runWidgetEvents(XY render.Point, ev *event.State,
|
|||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
handle(w.Event(MouseOut, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.hovering, id)
|
||||
}
|
||||
|
||||
if _, ok := s.clicked[id]; ok {
|
||||
handle(w.Event(MouseUp, EventData{
|
||||
Point: XY,
|
||||
Widget: w,
|
||||
Point: XY,
|
||||
}))
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
|
|
|
@ -119,6 +119,13 @@ func (w *TabFrame) SetTabsHidden(hidden bool) {
|
|||
}
|
||||
}
|
||||
|
||||
// Header returns access to the ui.Frame that holds the tab buttons. Use
|
||||
// at your own risk -- the UI arrangement in this Frame is not guaranteed
|
||||
// stable.
|
||||
func (w *TabFrame) Header() *Frame {
|
||||
return w.header
|
||||
}
|
||||
|
||||
// set the tab style between active and inactive
|
||||
func (w *TabFrame) setButtonStyle(button *Button, active bool) {
|
||||
var style = button.GetStyle()
|
||||
|
|
|
@ -75,6 +75,9 @@ type Widget interface {
|
|||
|
||||
// Render the final widget onto the drawing engine.
|
||||
Present(render.Engine, render.Point)
|
||||
|
||||
// Destroy: implement this if you have resources to free up on teardown.
|
||||
Destroy()
|
||||
}
|
||||
|
||||
// Config holds common base widget configs for quick configuration.
|
||||
|
@ -523,3 +526,6 @@ func (w *BaseWidget) Handle(event Event, fn func(EventData) error) {
|
|||
|
||||
// OnMouseOut should be overridden on widgets who want this event.
|
||||
func (w *BaseWidget) OnMouseOut(render.Point) {}
|
||||
|
||||
// Destroy does nothing on the base widget. Implement it for widgets which need it.
|
||||
func (w *BaseWidget) Destroy() {}
|
||||
|
|
19
window.go
19
window.go
|
@ -321,6 +321,20 @@ func (w *Window) Size() render.Rect {
|
|||
return w.body.Size()
|
||||
}
|
||||
|
||||
// Resize the window.
|
||||
func (w *Window) Resize(size render.Rect) {
|
||||
w.BaseWidget.Resize(size)
|
||||
w.body.Resize(size)
|
||||
}
|
||||
|
||||
// Center the window on screen by providing your screen (app window) size.
|
||||
func (w *Window) Center(width, height int) {
|
||||
w.MoveTo(render.Point{
|
||||
X: (width / 2) - (w.Size().W / 2),
|
||||
Y: (height / 2) - (w.Size().H / 2),
|
||||
})
|
||||
}
|
||||
|
||||
// Close the window, hiding it from display and calling its CloseWindow handler.
|
||||
func (w *Window) Close() {
|
||||
w.Hide()
|
||||
|
@ -395,3 +409,8 @@ func (w *Window) Present(e render.Engine, P render.Point) {
|
|||
// Call the BaseWidget Present in case we have subscribers.
|
||||
w.BaseWidget.Present(e, P)
|
||||
}
|
||||
|
||||
// Destroy hides the window.
|
||||
func (w *Window) Destroy() {
|
||||
w.Hide()
|
||||
}
|
||||
|
|
|
@ -173,6 +173,7 @@ func (s *Supervisor) CloseAllWindows() int {
|
|||
)
|
||||
for node != nil {
|
||||
i++
|
||||
node.window.Destroy()
|
||||
node.window.Hide()
|
||||
node = node.next
|
||||
}
|
||||
|
@ -183,8 +184,10 @@ func (s *Supervisor) CloseAllWindows() int {
|
|||
func (s *Supervisor) CloseActiveWindow() bool {
|
||||
var node = s.winFocus
|
||||
if node != nil {
|
||||
node.window.Destroy()
|
||||
node.window.Hide()
|
||||
s.winFocus = node.next
|
||||
s.winFocus.window.SetFocus(true)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
Loading…
Reference in New Issue
Block a user