Add README, LICENSE and Examples
This commit is contained in:
parent
e391e703bf
commit
2f5b498ca1
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Noah Petherbridge
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
236
README.md
Normal file
236
README.md
Normal file
|
@ -0,0 +1,236 @@
|
|||
# ui: User Interface Toolkit for Go
|
||||
|
||||
Package ui is a user interface toolkit for Go that targets desktop
|
||||
applications (SDL2, for Linux, MacOS and Windows) as well as web browsers
|
||||
(WebAssembly rendering to an HTML Canvas).
|
||||
|
||||
![Screenshot](docs/guitest.png)
|
||||
|
||||
> _(Screenshot is from Project: Doodle's GUITest debug screen showing a_
|
||||
> _Window, several Frames, Labels, Buttons and a Checkbox widget.)_
|
||||
|
||||
It is very much a **work in progress** and it's a bit buggy. See the
|
||||
[Known Issues](#known-issues) at the bottom of this document.
|
||||
|
||||
This library is being developed in conjunction with my drawing-based maze
|
||||
game, [Project: Doodle](https://www.kirsle.net/doodle). The rendering engine
|
||||
library is at [go/render](https://git.kirsle.net/go/render) which provides
|
||||
the SDL2 and Canvas back-ends.
|
||||
(GitHub mirror: [kirsle/render](https://github.com/kirsle/render))
|
||||
|
||||
# Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mw, err := ui.NewMainWindow("Hello World")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mw.SetBackground(render.White)
|
||||
|
||||
// Draw a label.
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: "Hello, world!",
|
||||
Font: render.Text{
|
||||
FontFilename: "../DejaVuSans.ttf",
|
||||
Size: 32,
|
||||
Color: render.SkyBlue,
|
||||
Shadow: render.SkyBlue.Darken(40),
|
||||
},
|
||||
})
|
||||
mw.Pack(label, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
PadY: 12,
|
||||
})
|
||||
|
||||
// Draw a button.
|
||||
button := ui.NewButton("My Button", ui.NewLabel(ui.Label{
|
||||
Text: "Click me!",
|
||||
Font: render.Text{
|
||||
FontFilename: "../DejaVuSans.ttf",
|
||||
Size: 12,
|
||||
Color: render.Red,
|
||||
Padding: 4,
|
||||
},
|
||||
}))
|
||||
button.Handle(ui.Click, func(p render.Point) {
|
||||
fmt.Println("I've been clicked!")
|
||||
})
|
||||
mw.Pack(button, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
})
|
||||
|
||||
// Add the button to the MainWindow's Supervisor so it can be
|
||||
// clicked on and interacted with.
|
||||
mw.Add(button)
|
||||
|
||||
mw.MainLoop()
|
||||
}
|
||||
```
|
||||
|
||||
# Widgets and Features
|
||||
|
||||
The following widgets have been implemented or are planned for the future.
|
||||
|
||||
Widgets are designed to be composable, making use of pre-existing widgets to
|
||||
create more complex ones. The widgets here are ordered from simplest to
|
||||
most complex.
|
||||
|
||||
**Fully implemented widgets:**
|
||||
|
||||
* [x] **BaseWidget**: the base class of all Widgets.
|
||||
* The `Widget` interface describes the functions common to all Widgets,
|
||||
such as SetBackground, Configure, MoveTo, Resize, and so on.
|
||||
* BaseWidget provides sane default implementations for all the methods
|
||||
required by the Widget interface. Most Widgets inherit from
|
||||
the BaseWidget.
|
||||
* [x] **Frame**: a layout wrapper for other widgets.
|
||||
* 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.
|
||||
* [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
|
||||
string or int reference, respectively, to provide the text of the label
|
||||
dynamically.
|
||||
* [x] **Image**: show a PNG or Bitmap image on your UI.
|
||||
* [x] **Button**: clickable buttons.
|
||||
* They can wrap _any_ widget. Labels are most common but can also wrap a
|
||||
Frame so you can have labels + icon images inside the button, etc.
|
||||
* Mouse hover and click event handlers.
|
||||
* [x] **CheckButton** and **RadioButton**
|
||||
* Variants on the Button which bind to a variable and toggle its state
|
||||
when clicked. Boolean variable pointers are used with CheckButton and
|
||||
string pointers for RadioButton.
|
||||
* CheckButtons stay pressed in when clicked (true) and pop back out when
|
||||
clicked again (false).
|
||||
* RadioButtons stay pressed in when the string variable matches their
|
||||
value, and pop out when the string variable changes.
|
||||
* [x] **Checkbox** and **Radiobox**: a Frame widget that wraps a
|
||||
CheckButton and a Label to provide a more traditional UI element.
|
||||
* Works the same as CheckButton and RadioButton but draws a separate
|
||||
label next to a small check button. Clicking the label will toggle the
|
||||
state of the checkbox.
|
||||
* [x] **Window**: a Frame with a title bar Frame on top.
|
||||
* Note: Window is not yet draggable or closeable.
|
||||
|
||||
**Work in progress widgets:**
|
||||
|
||||
* [x] **Menu**: a frame with clickable menu items.
|
||||
* To be a base widget behind right-click context menus, pull-down menus
|
||||
from a MenuBar, options from a SelectBox and so on.
|
||||
* Powered by Frame and Button but with a nice API for composing menu
|
||||
actions.
|
||||
* Partially implemented so far.
|
||||
* [ ] **MenuButton**: a Button that opens a Menu when clicked.
|
||||
* [ ] **MenuBar**: a Frame that houses many MenuButtons, intended for the
|
||||
main menu at the top of a UI window (File, Edit, Help, etc.).
|
||||
* [ ] **Scrollbar**: a Frame including a trough, scroll buttons and a
|
||||
draggable slider.
|
||||
|
||||
**Wish list for the longer-term future:**
|
||||
|
||||
* [ ] **SelectBox:** a kind of MenuButton that lets the user choose a value
|
||||
from a list of possible values, bound to a string variable.
|
||||
* [ ] **WindowManager**: manages Window widgets and focus support for all
|
||||
interactable widgets.
|
||||
* Would enable Windows to be dragged around by their title bar, overlap
|
||||
other Windows, and rise on top of other Windows when clicked.
|
||||
* Would enable "focus" support for Buttons, Text Boxes and other
|
||||
interactable widgets.
|
||||
* [ ] **TextBox:** an editable text field that the user can focus and type
|
||||
a value into.
|
||||
* Would depend on the WindowManager to manage focus for the widgets.
|
||||
|
||||
## Supervisor for Interaction
|
||||
|
||||
Some widgets that support user interaction (such as Button, CheckButton and
|
||||
Checkbox) need to be added to the Supervisor which watches over them and
|
||||
communicates events that they're interested in.
|
||||
|
||||
```go
|
||||
func SupervisorSDL2Example() {
|
||||
// NOTE: using the render/sdl engine.
|
||||
window := sdl.New("Hello World", 800, 600)
|
||||
window.Setup()
|
||||
|
||||
// One Supervisor is needed per UI.
|
||||
supervisor := ui.NewSupervisor()
|
||||
|
||||
// A button for our UI.
|
||||
btn := ui.NewButton("Button1", ui.NewLabel(ui.Label{
|
||||
Text: "Click me!",
|
||||
}))
|
||||
|
||||
// Add it to the Supervisor.
|
||||
supervisor.Add(btn)
|
||||
|
||||
// Main loop
|
||||
for {
|
||||
// Check for keyboard/mouse events
|
||||
ev, _ := window.Poll()
|
||||
|
||||
// Ping the Supervisor Loop function with the event state, so
|
||||
// it can trigger events on the widgets under its care.
|
||||
supervisor.Loop(ev)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You only need one Supervisor instance per UI. Add() each interactive widget
|
||||
to it, and call its Loop() method in your main loop so it can update the
|
||||
state of the widgets under its care.
|
||||
|
||||
The MainWindow includes its own Supervisor, see below.
|
||||
|
||||
## MainWindow for Simple Applications
|
||||
|
||||
The MainWindow widget may be used for "simple" UI applications where all you
|
||||
want is a GUI and you don't want to manage your own SDL2 (or Canvas) engine.
|
||||
|
||||
MainWindow is only to be used **one time** per application, and it sets up
|
||||
its own SDL2 render context and creates the main window. It also contains a
|
||||
Frame widget for the window contents and you may Pack() widgets into the
|
||||
window the same as you would a Frame.
|
||||
|
||||
MainWindow includes its own Supervisor: just call the `.Add(Widget)`
|
||||
method to add interactive widgets to the supervisor. The MainLoop() of the
|
||||
window calls Supervisor.Loop() automatically.
|
||||
|
||||
# Known Issues
|
||||
|
||||
The frame packing algorithm (frame_pack.go) is currently very buggy and in
|
||||
need of a re-write. Some examples of issues with it:
|
||||
|
||||
* Currently, when the Frame is iterating over packed widgets to decide their
|
||||
location and size, it explicitly calls MoveTo() and Resize() giving them
|
||||
their pixel-coordinates, relative to the Frame's own position.
|
||||
* When Frames nest other Frames this becomes more of an issue.
|
||||
* The Supervisor sometimes can't determine the correct position of a
|
||||
button packed inside of nested frames. It currently checks the
|
||||
Point() of the button (set by its parent Frame) and this doesn't
|
||||
account for the grandparent frame's position. Using the
|
||||
AbsolutePosition() helper function (which recursively crawls up a
|
||||
widget tree) also yields incorrect results, as the position of each
|
||||
Frame is _added_ to the position of the Button which throws it off even
|
||||
further.
|
||||
|
||||
It's on my to-do list to rewrite the algorithm from scratch and make it
|
||||
more resilient. One thing I also want to do is rename the `Anchor` field
|
||||
and call it `Side` to be more in line with the Tk GUI toolkit's naming
|
||||
convention. ("Side: N" or "Side: SE", and let the "Anchor" name be used for
|
||||
how to center the widget inside of its space ("Top", "Center", "Left", etc.)
|
||||
|
||||
# License
|
||||
|
||||
MIT.
|
BIN
docs/guitest.png
Normal file
BIN
docs/guitest.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
eg/DejaVuSans.ttf
Normal file
BIN
eg/DejaVuSans.ttf
Normal file
Binary file not shown.
55
eg/hello-world/main.go
Normal file
55
eg/hello-world/main.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mw, err := ui.NewMainWindow("Hello World")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mw.SetBackground(render.White)
|
||||
|
||||
// Draw a label.
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: "Hello, world!",
|
||||
Font: render.Text{
|
||||
FontFilename: "../DejaVuSans.ttf",
|
||||
Size: 32,
|
||||
Color: render.SkyBlue,
|
||||
Shadow: render.SkyBlue.Darken(40),
|
||||
},
|
||||
})
|
||||
mw.Pack(label, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
PadY: 12,
|
||||
})
|
||||
|
||||
// Draw a button.
|
||||
button := ui.NewButton("My Button", ui.NewLabel(ui.Label{
|
||||
Text: "Click me!",
|
||||
Font: render.Text{
|
||||
FontFilename: "../DejaVuSans.ttf",
|
||||
Size: 12,
|
||||
Color: render.Red,
|
||||
Padding: 4,
|
||||
},
|
||||
}))
|
||||
button.Handle(ui.Click, func(p render.Point) {
|
||||
fmt.Println("I've been clicked!")
|
||||
})
|
||||
mw.Pack(button, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
})
|
||||
|
||||
// Add the button to the MainWindow's Supervisor so it can be
|
||||
// clicked on and interacted with.
|
||||
mw.Add(button)
|
||||
|
||||
mw.MainLoop()
|
||||
}
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/ui/eg/layout"
|
||||
"git.kirsle.net/go/ui/eg/layout"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
102
eg/main.go
102
eg/main.go
|
@ -1,8 +1,10 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/apps/doodle/lib/ui"
|
||||
"git.kirsle.net/go/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -13,7 +15,7 @@ func main() {
|
|||
|
||||
leftFrame := ui.NewFrame("Left Frame")
|
||||
leftFrame.Configure(ui.Config{
|
||||
Width: 200,
|
||||
Width: 160,
|
||||
BorderSize: 1,
|
||||
BorderStyle: ui.BorderRaised,
|
||||
Background: render.Grey,
|
||||
|
@ -37,7 +39,37 @@ func main() {
|
|||
Text: "Hello world",
|
||||
})
|
||||
leftFrame.Pack(label, ui.Pack{
|
||||
Anchor: ui.SE,
|
||||
Anchor: ui.N,
|
||||
PadY: 12,
|
||||
})
|
||||
|
||||
// Draw some buttons in the left frame.
|
||||
for i := 1; i <= 12; i++ {
|
||||
i := i
|
||||
|
||||
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) {
|
||||
fmt.Printf("Button %d was clicked\n", i)
|
||||
})
|
||||
|
||||
// Add the button to the MainWindow's event supervisor, so it may be
|
||||
// clicked and interacted with.
|
||||
mw.Add(btn)
|
||||
|
||||
leftFrame.Pack(btn, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
PadY: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// Frame to show off check buttons.
|
||||
mainFrame.Pack(radioButtonFrame(mw), ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
PadY: 8,
|
||||
})
|
||||
|
||||
err = mw.MainLoop()
|
||||
|
@ -45,3 +77,67 @@ func main() {
|
|||
panic("MainLoop:" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Frame that shows off radio buttons.
|
||||
func radioButtonFrame(mw *ui.MainWindow) *ui.Frame {
|
||||
// The string variable that will be bound to the radio buttons.
|
||||
// This could also be a global variable at the package level.
|
||||
radioValue := "Red"
|
||||
|
||||
// Main frame.
|
||||
frame := ui.NewFrame("radio button demo")
|
||||
frame.Configure(ui.Config{
|
||||
Background: render.RGBA(153, 255, 153, 255),
|
||||
BorderSize: 1,
|
||||
BorderStyle: ui.BorderRaised,
|
||||
})
|
||||
|
||||
// Top row to show the label and current radiobutton bound value.
|
||||
topFrame := ui.NewFrame("radio button label frame")
|
||||
frame.Pack(topFrame, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
|
||||
// Draw the labels.
|
||||
{
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: "Radio buttons. Value:",
|
||||
})
|
||||
topFrame.Pack(label, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
})
|
||||
|
||||
valueLabel := ui.NewLabel(ui.Label{
|
||||
TextVariable: &radioValue,
|
||||
})
|
||||
topFrame.Pack(valueLabel, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
PadX: 4,
|
||||
})
|
||||
}
|
||||
|
||||
// The radio buttons themselves.
|
||||
btnFrame := ui.NewFrame("radio button frame")
|
||||
frame.Pack(btnFrame, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
{
|
||||
colors := []string{"Red", "Green", "Blue", "Yellow"}
|
||||
for _, color := range colors {
|
||||
color := color
|
||||
|
||||
btn := ui.NewRadioButton("color:"+color, &radioValue, color, ui.NewLabel(ui.Label{
|
||||
Text: color,
|
||||
}))
|
||||
mw.Add(btn)
|
||||
btnFrame.Pack(btn, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
PadX: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ var (
|
|||
|
||||
// MainWindow is the parent window of a UI application.
|
||||
type MainWindow struct {
|
||||
engine render.Engine
|
||||
Engine render.Engine
|
||||
supervisor *Supervisor
|
||||
frame *Frame
|
||||
w int
|
||||
|
@ -33,12 +33,12 @@ func NewMainWindow(title string) (*MainWindow, error) {
|
|||
supervisor: NewSupervisor(),
|
||||
}
|
||||
|
||||
mw.engine = sdl.New(
|
||||
mw.Engine = sdl.New(
|
||||
title,
|
||||
mw.w,
|
||||
mw.h,
|
||||
)
|
||||
if err := mw.engine.Setup(); err != nil {
|
||||
if err := mw.Engine.Setup(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -72,9 +72,14 @@ func (mw *MainWindow) resized() {
|
|||
})
|
||||
}
|
||||
|
||||
// SetBackground changes the window's frame's background color.
|
||||
func (mw *MainWindow) SetBackground(color render.Color) {
|
||||
mw.frame.SetBackground(color)
|
||||
}
|
||||
|
||||
// Present the window.
|
||||
func (mw *MainWindow) Present() {
|
||||
mw.supervisor.Present(mw.engine)
|
||||
mw.supervisor.Present(mw.Engine)
|
||||
}
|
||||
|
||||
// MainLoop starts the main event loop and blocks until there's an error.
|
||||
|
@ -89,19 +94,19 @@ func (mw *MainWindow) MainLoop() error {
|
|||
|
||||
// Loop does one loop of the UI.
|
||||
func (mw *MainWindow) Loop() error {
|
||||
mw.engine.Clear(render.White)
|
||||
mw.Engine.Clear(render.White)
|
||||
|
||||
// Record how long this loop took.
|
||||
start := time.Now()
|
||||
|
||||
// Poll for events.
|
||||
ev, err := mw.engine.Poll()
|
||||
ev, err := mw.Engine.Poll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("event poll error: %s", err)
|
||||
}
|
||||
|
||||
if ev.WindowResized {
|
||||
w, h := mw.engine.WindowSize()
|
||||
w, h := mw.Engine.WindowSize()
|
||||
if w != mw.w || h != mw.h {
|
||||
mw.w = w
|
||||
mw.h = h
|
||||
|
@ -109,11 +114,12 @@ func (mw *MainWindow) Loop() error {
|
|||
}
|
||||
}
|
||||
|
||||
mw.frame.Compute(mw.engine)
|
||||
mw.frame.Compute(mw.Engine)
|
||||
|
||||
// Render the child widgets.
|
||||
mw.supervisor.Present(mw.engine)
|
||||
mw.engine.Present()
|
||||
mw.supervisor.Loop(ev)
|
||||
mw.supervisor.Present(mw.Engine)
|
||||
mw.Engine.Present()
|
||||
|
||||
// Delay to maintain target frames per second.
|
||||
var delay uint32
|
||||
|
@ -122,7 +128,7 @@ func (mw *MainWindow) Loop() error {
|
|||
if targetFPS-int(elapsed) > 0 {
|
||||
delay = uint32(targetFPS - int(elapsed))
|
||||
}
|
||||
mw.engine.Delay(delay)
|
||||
mw.Engine.Delay(delay)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user