Browse Source

Add README, LICENSE and Examples

menus
Noah Petherbridge 7 months ago
parent
commit
2f5b498ca1
8 changed files with 429 additions and 15 deletions
  1. +21
    -0
      LICENSE.md
  2. +236
    -0
      README.md
  3. BIN
      docs/guitest.png
  4. BIN
      eg/DejaVuSans.ttf
  5. +55
    -0
      eg/hello-world/main.go
  6. +1
    -1
      eg/layout/main.go
  7. +99
    -3
      eg/main.go
  8. +17
    -11
      main_window.go

+ 21
- 0
LICENSE.md View 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
- 0
README.md View 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 View File

Before After
Width: 802  |  Height: 637  |  Size: 31 KiB

BIN
eg/DejaVuSans.ttf View File


+ 55
- 0
eg/hello-world/main.go View 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()
}

+ 1
- 1
eg/layout/main.go View File

@@ -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() {


+ 99
- 3
eg/main.go View File

@@ -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
- 11
main_window.go View File

@@ -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…
Cancel
Save