Noah Petherbridge
5434484b6e
The `level.Canvas` is a widget that holds onto its Palette and Grid and has interactions to allow scrolling and editing the grid using the swatches available on the palette. Thus all of the logic in the Editor Mode for drawing directly onto the root SDL surface are now handled inside a level.Canvas instance. The `level.Canvas` widget has the following properties: * Like any widget it has an X,Y position and a width/height. * It has a Scroll position to control which slice of its drawing will be visible inside its bounding box. * It supports levels having negative coordinates for their pixels. It doesn't care. The default Scroll position is (0,0) at the top left corner of the widget but you can scroll into the negatives and see the negative pixels. * Keyboard keys will scroll the viewport inside the canvas. * The canvas draws only the pixels that are visible inside its bounding box. This feature will eventually pave the way toward: * Doodads being dropped on top of your map, each Doodad being its own Canvas widget. * Using drawings as button icons for the user interface, as the Canvas is a normal widget.
432 lines
11 KiB
Go
432 lines
11 KiB
Go
package ui
|
|
|
|
import (
|
|
"git.kirsle.net/apps/doodle/render"
|
|
"git.kirsle.net/apps/doodle/ui/theme"
|
|
)
|
|
|
|
// BorderStyle options for widget.SetBorderStyle()
|
|
type BorderStyle string
|
|
|
|
// Styles for a widget border.
|
|
const (
|
|
BorderNone BorderStyle = ""
|
|
BorderSolid BorderStyle = "solid"
|
|
BorderRaised = "raised"
|
|
BorderSunken = "sunken"
|
|
)
|
|
|
|
// Widget is a user interface element.
|
|
type Widget interface {
|
|
ID() string // Get the widget's string ID.
|
|
IDFunc(func() string) // Set a function that returns the widget's ID.
|
|
String() string
|
|
Point() render.Point
|
|
MoveTo(render.Point)
|
|
MoveBy(render.Point)
|
|
Size() render.Rect // Return the Width and Height of the widget.
|
|
FixedSize() bool // Return whether the size is fixed (true) or automatic (false)
|
|
BoxSize() render.Rect // Return the full size including the border and outline.
|
|
Resize(render.Rect)
|
|
ResizeBy(render.Rect)
|
|
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
|
|
|
Handle(Event, func(render.Point))
|
|
Event(Event, render.Point) // called internally to trigger an event
|
|
|
|
// Thickness of the padding + border + outline.
|
|
BoxThickness(multiplier int32) int32
|
|
DrawBox(render.Engine, render.Point)
|
|
|
|
// Widget configuration getters.
|
|
Margin() int32 // Margin away from other widgets
|
|
SetMargin(int32) //
|
|
Background() render.Color // Background color
|
|
SetBackground(render.Color) //
|
|
Foreground() render.Color // Foreground color
|
|
SetForeground(render.Color) //
|
|
BorderStyle() BorderStyle // Border style: none, raised, sunken
|
|
SetBorderStyle(BorderStyle) //
|
|
BorderColor() render.Color // Border color (default is Background)
|
|
SetBorderColor(render.Color) //
|
|
BorderSize() int32 // Border size (default 0)
|
|
SetBorderSize(int32) //
|
|
OutlineColor() render.Color // Outline color (default Invisible)
|
|
SetOutlineColor(render.Color) //
|
|
OutlineSize() int32 // Outline size (default 0)
|
|
SetOutlineSize(int32) //
|
|
|
|
// Run any render computations; by the end the widget must know its
|
|
// Width and Height. For example the Label widget will render itself onto
|
|
// an SDL Surface and then it will know its bounding box, but not before.
|
|
Compute(render.Engine)
|
|
|
|
// Render the final widget onto the drawing engine.
|
|
Present(render.Engine, render.Point)
|
|
}
|
|
|
|
// Config holds common base widget configs for quick configuration.
|
|
type Config struct {
|
|
// Size management. If you provide a non-zero value for Width and Height,
|
|
// the widget will be resized and the "fixedSize" flag is set, meaning it
|
|
// will not re-compute its size dynamically. To set the size while also
|
|
// keeping the auto-resize property, pass AutoResize=true too. This is
|
|
// mainly used internally when widgets are calculating their automatic sizes.
|
|
AutoResize bool
|
|
Width int32
|
|
Height int32
|
|
Margin int32
|
|
MarginX int32
|
|
MarginY int32
|
|
Background render.Color
|
|
Foreground render.Color
|
|
BorderSize int32
|
|
BorderStyle BorderStyle
|
|
BorderColor render.Color
|
|
OutlineSize int32
|
|
OutlineColor render.Color
|
|
}
|
|
|
|
// BaseWidget holds common functionality for all widgets, such as managing
|
|
// their widths and heights.
|
|
type BaseWidget struct {
|
|
id string
|
|
idFunc func() string
|
|
fixedSize bool
|
|
width int32
|
|
height int32
|
|
point render.Point
|
|
margin int32
|
|
background render.Color
|
|
foreground render.Color
|
|
borderStyle BorderStyle
|
|
borderColor render.Color
|
|
borderSize int32
|
|
outlineColor render.Color
|
|
outlineSize int32
|
|
handlers map[Event][]func(render.Point)
|
|
}
|
|
|
|
// SetID sets a string name for your widget, helpful for debugging purposes.
|
|
func (w *BaseWidget) SetID(id string) {
|
|
w.id = id
|
|
}
|
|
|
|
// ID returns the ID that the widget calls itself by.
|
|
func (w *BaseWidget) ID() string {
|
|
if w.idFunc == nil {
|
|
w.IDFunc(func() string {
|
|
return "Widget<Untitled>"
|
|
})
|
|
}
|
|
return w.idFunc()
|
|
}
|
|
|
|
// IDFunc sets an ID function.
|
|
func (w *BaseWidget) IDFunc(fn func() string) {
|
|
w.idFunc = fn
|
|
}
|
|
|
|
func (w *BaseWidget) String() string {
|
|
return w.ID()
|
|
}
|
|
|
|
// Configure the base widget with all the common properties at once. Any
|
|
// property left as the zero value will not update the widget.
|
|
func (w *BaseWidget) Configure(c Config) {
|
|
if c.Width != 0 || c.Height != 0 {
|
|
w.fixedSize = !c.AutoResize
|
|
if c.Width != 0 {
|
|
w.width = c.Width
|
|
}
|
|
if c.Height != 0 {
|
|
w.height = c.Height
|
|
}
|
|
}
|
|
|
|
if c.Margin != 0 {
|
|
w.margin = c.Margin
|
|
}
|
|
if c.Background != render.Invisible {
|
|
w.background = c.Background
|
|
}
|
|
if c.Foreground != render.Invisible {
|
|
w.foreground = c.Foreground
|
|
}
|
|
if c.BorderColor != render.Invisible {
|
|
w.borderColor = c.BorderColor
|
|
}
|
|
if c.OutlineColor != render.Invisible {
|
|
w.outlineColor = c.OutlineColor
|
|
}
|
|
|
|
if c.BorderSize != 0 {
|
|
w.borderSize = c.BorderSize
|
|
}
|
|
if c.BorderStyle != BorderNone {
|
|
w.borderStyle = c.BorderStyle
|
|
}
|
|
if c.OutlineSize != 0 {
|
|
w.outlineSize = c.OutlineSize
|
|
}
|
|
}
|
|
|
|
// Rect returns the widget's absolute rectangle, the combined Size and Point.
|
|
func (w *BaseWidget) Rect() render.Rect {
|
|
return render.Rect{
|
|
X: w.point.X,
|
|
Y: w.point.Y,
|
|
W: w.width,
|
|
H: w.height,
|
|
}
|
|
}
|
|
|
|
// Point returns the X,Y position of the widget on the window.
|
|
func (w *BaseWidget) Point() render.Point {
|
|
return w.point
|
|
}
|
|
|
|
// MoveTo updates the X,Y position to the new point.
|
|
func (w *BaseWidget) MoveTo(v render.Point) {
|
|
w.point = v
|
|
}
|
|
|
|
// MoveBy adds the X,Y values to the widget's current position.
|
|
func (w *BaseWidget) MoveBy(v render.Point) {
|
|
w.point.X += v.X
|
|
w.point.Y += v.Y
|
|
}
|
|
|
|
// Size returns the box with W and H attributes containing the size of the
|
|
// widget. The X,Y attributes of the box are ignored and zero.
|
|
func (w *BaseWidget) Size() render.Rect {
|
|
return render.Rect{
|
|
W: w.width,
|
|
H: w.height,
|
|
}
|
|
}
|
|
|
|
// BoxSize returns the full rendered size of the widget including its box
|
|
// thickness (border, padding and outline).
|
|
func (w *BaseWidget) BoxSize() render.Rect {
|
|
return render.Rect{
|
|
W: w.width + w.BoxThickness(2),
|
|
H: w.height + w.BoxThickness(2),
|
|
}
|
|
}
|
|
|
|
// FixedSize returns whether the widget's size has been hard-coded by the user
|
|
// (true) or if it automatically resizes based on its contents (false).
|
|
func (w *BaseWidget) FixedSize() bool {
|
|
return w.fixedSize
|
|
}
|
|
|
|
// Resize sets the size of the widget to the .W and .H attributes of a rect.
|
|
func (w *BaseWidget) Resize(v render.Rect) {
|
|
w.fixedSize = true
|
|
w.width = v.W
|
|
w.height = v.H
|
|
}
|
|
|
|
// ResizeBy resizes by a relative amount.
|
|
func (w *BaseWidget) ResizeBy(v render.Rect) {
|
|
w.fixedSize = true
|
|
w.width += v.W
|
|
w.height += v.H
|
|
}
|
|
|
|
// resizeAuto sets the size of the widget but doesn't set the fixedSize flag.
|
|
func (w *BaseWidget) resizeAuto(v render.Rect) {
|
|
w.width = v.W
|
|
w.height = v.H
|
|
}
|
|
|
|
// BoxThickness returns the full sum of the padding, border and outline.
|
|
// m = multiplier, i.e., 1 or 2
|
|
func (w *BaseWidget) BoxThickness(m int32) int32 {
|
|
if m == 0 {
|
|
m = 1
|
|
}
|
|
return (w.Margin() * m) + (w.BorderSize() * m) + (w.OutlineSize() * m)
|
|
}
|
|
|
|
// DrawBox draws the border and outline.
|
|
func (w *BaseWidget) DrawBox(e render.Engine, P render.Point) {
|
|
var (
|
|
S = w.Size()
|
|
outline = w.OutlineSize()
|
|
border = w.BorderSize()
|
|
borderColor = w.BorderColor()
|
|
highlight = borderColor.Lighten(theme.BorderColorOffset)
|
|
shadow = borderColor.Darken(theme.BorderColorOffset)
|
|
color render.Color
|
|
box = render.Rect{
|
|
X: P.X,
|
|
Y: P.Y,
|
|
W: S.W,
|
|
H: S.H,
|
|
}
|
|
)
|
|
|
|
if borderColor == render.Invisible {
|
|
borderColor = render.Red
|
|
}
|
|
|
|
// Draw the outline layer as the full size of the widget.
|
|
if outline > 0 && w.OutlineColor() != render.Invisible {
|
|
e.DrawBox(w.OutlineColor(), render.Rect{
|
|
X: P.X,
|
|
Y: P.Y,
|
|
W: S.W,
|
|
H: S.H,
|
|
})
|
|
}
|
|
box.X += outline
|
|
box.Y += outline
|
|
box.W -= outline * 2
|
|
box.H -= outline * 2
|
|
|
|
// Highlight on the top left edge.
|
|
if border > 0 {
|
|
if w.BorderStyle() == BorderRaised {
|
|
color = highlight
|
|
} else if w.BorderStyle() == BorderSunken {
|
|
color = shadow
|
|
} else {
|
|
color = borderColor
|
|
}
|
|
e.DrawBox(color, box)
|
|
}
|
|
|
|
// Shadow on the bottom right edge.
|
|
box.X += border
|
|
box.Y += border
|
|
box.W -= border
|
|
box.H -= border
|
|
if w.BorderSize() > 0 {
|
|
if w.BorderStyle() == BorderRaised {
|
|
color = shadow
|
|
} else if w.BorderStyle() == BorderSunken {
|
|
color = highlight
|
|
} else {
|
|
color = borderColor
|
|
}
|
|
e.DrawBox(color, box)
|
|
}
|
|
|
|
// Background color of the button.
|
|
box.W -= border
|
|
box.H -= border
|
|
if w.Background() != render.Invisible {
|
|
e.DrawBox(w.Background(), box)
|
|
}
|
|
}
|
|
|
|
// Margin returns the margin width.
|
|
func (w *BaseWidget) Margin() int32 {
|
|
return w.margin
|
|
}
|
|
|
|
// SetMargin sets the margin width.
|
|
func (w *BaseWidget) SetMargin(v int32) {
|
|
w.margin = v
|
|
}
|
|
|
|
// Background returns the background color.
|
|
func (w *BaseWidget) Background() render.Color {
|
|
return w.background
|
|
}
|
|
|
|
// SetBackground sets the color.
|
|
func (w *BaseWidget) SetBackground(c render.Color) {
|
|
w.background = c
|
|
}
|
|
|
|
// Foreground returns the foreground color.
|
|
func (w *BaseWidget) Foreground() render.Color {
|
|
return w.foreground
|
|
}
|
|
|
|
// SetForeground sets the color.
|
|
func (w *BaseWidget) SetForeground(c render.Color) {
|
|
w.foreground = c
|
|
}
|
|
|
|
// BorderStyle returns the border style.
|
|
func (w *BaseWidget) BorderStyle() BorderStyle {
|
|
return w.borderStyle
|
|
}
|
|
|
|
// SetBorderStyle sets the border style.
|
|
func (w *BaseWidget) SetBorderStyle(v BorderStyle) {
|
|
w.borderStyle = v
|
|
}
|
|
|
|
// BorderColor returns the border color, or defaults to the background color.
|
|
func (w *BaseWidget) BorderColor() render.Color {
|
|
if w.borderColor == render.Invisible {
|
|
return w.Background()
|
|
}
|
|
return w.borderColor
|
|
}
|
|
|
|
// SetBorderColor sets the border color.
|
|
func (w *BaseWidget) SetBorderColor(c render.Color) {
|
|
w.borderColor = c
|
|
}
|
|
|
|
// BorderSize returns the border thickness.
|
|
func (w *BaseWidget) BorderSize() int32 {
|
|
return w.borderSize
|
|
}
|
|
|
|
// SetBorderSize sets the border thickness.
|
|
func (w *BaseWidget) SetBorderSize(v int32) {
|
|
w.borderSize = v
|
|
}
|
|
|
|
// OutlineColor returns the background color.
|
|
func (w *BaseWidget) OutlineColor() render.Color {
|
|
return w.outlineColor
|
|
}
|
|
|
|
// SetOutlineColor sets the color.
|
|
func (w *BaseWidget) SetOutlineColor(c render.Color) {
|
|
w.outlineColor = c
|
|
}
|
|
|
|
// OutlineSize returns the outline thickness.
|
|
func (w *BaseWidget) OutlineSize() int32 {
|
|
return w.outlineSize
|
|
}
|
|
|
|
// SetOutlineSize sets the outline thickness.
|
|
func (w *BaseWidget) SetOutlineSize(v int32) {
|
|
w.outlineSize = v
|
|
}
|
|
|
|
// Event is called internally by Doodle to trigger an event.
|
|
func (w *BaseWidget) Event(event Event, p render.Point) {
|
|
if handlers, ok := w.handlers[event]; ok {
|
|
for _, fn := range handlers {
|
|
fn(p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle an event in the widget.
|
|
func (w *BaseWidget) Handle(event Event, fn func(render.Point)) {
|
|
if w.handlers == nil {
|
|
w.handlers = map[Event][]func(render.Point){}
|
|
}
|
|
|
|
if _, ok := w.handlers[event]; !ok {
|
|
w.handlers[event] = []func(render.Point){}
|
|
}
|
|
|
|
w.handlers[event] = append(w.handlers[event], fn)
|
|
}
|
|
|
|
// OnMouseOut should be overridden on widgets who want this event.
|
|
func (w *BaseWidget) OnMouseOut(render.Point) {}
|