parent
e391e703bf
commit
2f5b498ca1
8 changed files with 429 additions and 15 deletions
@ -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. |
@ -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 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. |
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
@ -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() |
||||
} |
Loading…
Reference in new issue