Text Tool and Pan Tool
Two new tools added to the Level Editor: * Pan Tool: left-click to scroll the level around safely. * Text Tool: write text onto your level. Features of the Text Tool: * Can choose from the game's built-in fonts, size and enter the message you want to write. * The mouse cursor previews the text when hovered over the level. * Click to "stamp" the text onto your level. The currently selected color swatch will be used to color the text in. * Adds two new fonts: Azulian.ttf and Rive.ttf that can be selected in the Text Tool. Some implementation notes: * Added package native/engine_sdl.go that handles the lower-level SDL2_TTF logic to rasterize the text into a black&white image. * WASM not supported yet (if the game even still built for WASM); native/engine_wasm.go stubs out the TextToImage() call with a "not supported" error just in case. Other changes: * New Toolbar icons: they are 24x24 instead of 32x32 to make more room for more tools. * The toolbar now shows two buttons per row for a more densely packed layout. For very narrow screen widths (< 600px) the default Vertical Toolbar layout will use one-button-per-row to not eat too much screen real estate. * In the Horizontal Toolbars layout there are 2 buttons per column.
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 661 B |
Before Width: | Height: | Size: 717 B After Width: | Height: | Size: 662 B |
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 668 B |
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 619 B |
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 665 B |
BIN
assets/sprites/pan-tool.png
Normal file
After Width: | Height: | Size: 662 B |
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 689 B |
Before Width: | Height: | Size: 648 B After Width: | Height: | Size: 637 B |
BIN
assets/sprites/text-tool.png
Normal file
After Width: | Height: | Size: 639 B |
|
@ -59,6 +59,10 @@ var (
|
||||||
DefaultEraserBrushSize = 8
|
DefaultEraserBrushSize = 8
|
||||||
MaxEraserBrushSize = 32 // the bigger, the slower
|
MaxEraserBrushSize = 32 // the bigger, the slower
|
||||||
|
|
||||||
|
// Default font filename selected for Text Tool in the editor.
|
||||||
|
// TODO: better centralize font filenames, here and in theme.go
|
||||||
|
TextToolDefaultFont = "DejaVuSans.ttf"
|
||||||
|
|
||||||
// Interval for auto-save in the editor
|
// Interval for auto-save in the editor
|
||||||
AutoSaveInterval = 5 * time.Minute
|
AutoSaveInterval = 5 * time.Minute
|
||||||
|
|
||||||
|
|
56
pkg/drawtool/text_tool.go
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package drawtool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/native"
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextSettings holds currently selected Text Tool settings.
|
||||||
|
type TextSettings struct {
|
||||||
|
Font string // like 'DejaVuSans.ttf'
|
||||||
|
Size int
|
||||||
|
Message string
|
||||||
|
Label *ui.Label // cached label texture
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently active settings (global variable)
|
||||||
|
var TT TextSettings
|
||||||
|
|
||||||
|
// IsZero checks if the TextSettings are populated.
|
||||||
|
func (tt TextSettings) IsZero() bool {
|
||||||
|
return tt.Font == "" && tt.Size == 0 && tt.Message == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToStroke converts a TextSettings configuration into a Freehand
|
||||||
|
// Stroke, coloring in all of the pixels.
|
||||||
|
func (tt TextSettings) ToStroke(e render.Engine, color render.Color, at render.Point) (*Stroke, error) {
|
||||||
|
stroke := NewStroke(Freehand, color)
|
||||||
|
|
||||||
|
// Render the text to a Go image so we can get the colors from
|
||||||
|
// it uniformly.
|
||||||
|
img, err := native.TextToImage(e, tt.Label.Font)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull all its pixels.
|
||||||
|
var (
|
||||||
|
max = img.Bounds().Max
|
||||||
|
x = 0
|
||||||
|
y = 0
|
||||||
|
)
|
||||||
|
for x = 0; x < max.X; x++ {
|
||||||
|
for y = 0; y < max.Y; y++ {
|
||||||
|
hue := img.At(x, y)
|
||||||
|
r, g, b, _ := hue.RGBA()
|
||||||
|
if r == 65535 && g == r && b == r {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stroke.Points = append(stroke.Points, render.NewPoint(x+at.X, y+at.Y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stroke, nil
|
||||||
|
}
|
|
@ -12,6 +12,8 @@ const (
|
||||||
ActorTool // drag and move actors
|
ActorTool // drag and move actors
|
||||||
LinkTool
|
LinkTool
|
||||||
EraserTool
|
EraserTool
|
||||||
|
PanTool
|
||||||
|
TextTool
|
||||||
)
|
)
|
||||||
|
|
||||||
var toolNames = []string{
|
var toolNames = []string{
|
||||||
|
@ -22,6 +24,8 @@ var toolNames = []string{
|
||||||
"Doodad", // readable name for ActorTool
|
"Doodad", // readable name for ActorTool
|
||||||
"Link",
|
"Link",
|
||||||
"Eraser",
|
"Eraser",
|
||||||
|
"PanTool",
|
||||||
|
"TextTool",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Tool) String() string {
|
func (t Tool) String() string {
|
||||||
|
|
|
@ -51,6 +51,7 @@ type EditorUI struct {
|
||||||
doodadWindow *ui.Window
|
doodadWindow *ui.Window
|
||||||
paletteEditor *ui.Window
|
paletteEditor *ui.Window
|
||||||
layersWindow *ui.Window
|
layersWindow *ui.Window
|
||||||
|
textToolWindow *ui.Window
|
||||||
publishWindow *ui.Window
|
publishWindow *ui.Window
|
||||||
filesystemWindow *ui.Window
|
filesystemWindow *ui.Window
|
||||||
licenseWindow *ui.Window
|
licenseWindow *ui.Window
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/license"
|
"git.kirsle.net/apps/doodle/pkg/license"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
@ -49,6 +50,12 @@ func (u *EditorUI) OpenDoodadDropper() {
|
||||||
u.Supervisor.FocusWindow(u.doodadWindow)
|
u.Supervisor.FocusWindow(u.doodadWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpenTextTool opens the Text Tool window.
|
||||||
|
func (u *EditorUI) OpenTextTool() {
|
||||||
|
u.textToolWindow.Show()
|
||||||
|
u.Supervisor.FocusWindow(u.textToolWindow)
|
||||||
|
}
|
||||||
|
|
||||||
// OpenPublishWindow opens the Publisher window.
|
// OpenPublishWindow opens the Publisher window.
|
||||||
func (u *EditorUI) OpenPublishWindow() {
|
func (u *EditorUI) OpenPublishWindow() {
|
||||||
scene, _ := u.d.Scene.(*EditorScene)
|
scene, _ := u.d.Scene.(*EditorScene)
|
||||||
|
@ -148,6 +155,23 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.ConfigureWindow(d, u.doodadWindow)
|
u.ConfigureWindow(d, u.doodadWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text Tool window.
|
||||||
|
if u.textToolWindow == nil {
|
||||||
|
u.textToolWindow = windows.NewTextToolWindow(windows.TextTool{
|
||||||
|
Supervisor: u.Supervisor,
|
||||||
|
Engine: d.Engine,
|
||||||
|
OnChangeSettings: func(font string, size int, message string) {
|
||||||
|
log.Info("Updated Text Tool settings: %s (%d): %s", font, size, message)
|
||||||
|
drawtool.TT = drawtool.TextSettings{
|
||||||
|
Font: font,
|
||||||
|
Size: size,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
u.ConfigureWindow(d, u.textToolWindow)
|
||||||
|
}
|
||||||
|
|
||||||
// Page Settings
|
// Page Settings
|
||||||
if u.levelSettingsWindow == nil {
|
if u.levelSettingsWindow == nil {
|
||||||
scene, _ := d.Scene.(*EditorScene)
|
scene, _ := d.Scene.(*EditorScene)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package doodle
|
package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||||
|
@ -11,33 +13,54 @@ import (
|
||||||
"git.kirsle.net/go/ui/style"
|
"git.kirsle.net/go/ui/style"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Width of the toolbar frame.
|
// Global toolbarWidth, TODO: editor_ui.go wants it
|
||||||
var toolbarWidth = 44 // 38px button (32px sprite + borders) + padding
|
var toolbarWidth int
|
||||||
var toolbarSpriteSize = 32 // 32x32 sprites.
|
|
||||||
|
|
||||||
// SetupToolbar configures the UI for the Tools panel.
|
// SetupToolbar configures the UI for the Tools panel.
|
||||||
func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
// Horizontal toolbar instead of vertical?
|
// Horizontal toolbar instead of vertical?
|
||||||
var (
|
var (
|
||||||
isHoz = usercfg.Current.HorizontalToolbars
|
toolbarSpriteSize = 24 // size of sprite images
|
||||||
packAlign = ui.N
|
frameSize render.Rect
|
||||||
frameSize = render.NewRect(toolbarWidth, 100)
|
isHoz = usercfg.Current.HorizontalToolbars
|
||||||
tooltipEdge = ui.Right
|
buttonsPerRow = 2
|
||||||
btnPack = ui.Pack{
|
packAlign = ui.N
|
||||||
|
tooltipEdge = ui.Right
|
||||||
|
btnRowPack = ui.Pack{
|
||||||
Side: packAlign,
|
Side: packAlign,
|
||||||
PadY: 2,
|
PadY: 1,
|
||||||
|
Fill: true,
|
||||||
|
}
|
||||||
|
btnPack = ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
PadX: 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if isHoz {
|
if isHoz {
|
||||||
packAlign = ui.W
|
packAlign = ui.W
|
||||||
frameSize = render.NewRect(100, toolbarWidth)
|
|
||||||
tooltipEdge = ui.Bottom
|
tooltipEdge = ui.Bottom
|
||||||
btnPack = ui.Pack{
|
btnRowPack = ui.Pack{
|
||||||
Side: packAlign,
|
Side: packAlign,
|
||||||
PadX: 2,
|
PadX: 2,
|
||||||
}
|
}
|
||||||
|
btnPack = ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
PadY: 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button Layout Controls:
|
||||||
|
// We can draw 2 buttons per row, but for very small screens
|
||||||
|
// e.g. mobile in portrait orientation, draw 1 button per row.
|
||||||
|
buttonsPerRow = 1
|
||||||
|
if isHoz || d.width >= enum.ScreenWidthSmall {
|
||||||
|
buttonsPerRow = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute toolbar size to accommodate all buttons (+10 for borders/padding)
|
||||||
|
toolbarWidth = buttonsPerRow * (toolbarSpriteSize + 10)
|
||||||
|
frameSize = render.NewRect(toolbarWidth, 100)
|
||||||
|
|
||||||
frame := ui.NewFrame("Tool Bar")
|
frame := ui.NewFrame("Tool Bar")
|
||||||
frame.Resize(frameSize)
|
frame.Resize(frameSize)
|
||||||
frame.Configure(ui.Config{
|
frame.Configure(ui.Config{
|
||||||
|
@ -62,6 +85,16 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
// Optional fields.
|
// Optional fields.
|
||||||
NoDoodad bool // tool not available for Doodad editing (Levels only)
|
NoDoodad bool // tool not available for Doodad editing (Levels only)
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
Value: drawtool.PanTool.String(),
|
||||||
|
Icon: "assets/sprites/pan-tool.png",
|
||||||
|
Tooltip: "Pan Tool",
|
||||||
|
Click: func() {
|
||||||
|
u.Canvas.Tool = drawtool.PanTool
|
||||||
|
d.Flash("Pan Tool selected.")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
Value: drawtool.PencilTool.String(),
|
Value: drawtool.PencilTool.String(),
|
||||||
Icon: "assets/sprites/pencil-tool.png",
|
Icon: "assets/sprites/pencil-tool.png",
|
||||||
|
@ -102,6 +135,17 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Value: drawtool.TextTool.String(),
|
||||||
|
Icon: "assets/sprites/text-tool.png",
|
||||||
|
Tooltip: "Text Tool",
|
||||||
|
Click: func() {
|
||||||
|
u.Canvas.Tool = drawtool.TextTool
|
||||||
|
u.OpenTextTool()
|
||||||
|
d.Flash("Text Tool selected.")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
Value: drawtool.ActorTool.String(),
|
Value: drawtool.ActorTool.String(),
|
||||||
Icon: "assets/sprites/actor-tool.png",
|
Icon: "assets/sprites/actor-tool.png",
|
||||||
|
@ -146,12 +190,20 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, button := range buttons {
|
|
||||||
|
// Arrange the buttons 2x2.
|
||||||
|
var btnRow *ui.Frame
|
||||||
|
for i, button := range buttons {
|
||||||
button := button
|
button := button
|
||||||
if button.NoDoodad && u.Scene.DrawingType == enum.DoodadDrawing {
|
if button.NoDoodad && u.Scene.DrawingType == enum.DoodadDrawing {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if buttonsPerRow == 1 || i%buttonsPerRow == 0 {
|
||||||
|
btnRow = ui.NewFrame(fmt.Sprintf("Button Row %d", i))
|
||||||
|
btnFrame.Pack(btnRow, btnRowPack)
|
||||||
|
}
|
||||||
|
|
||||||
image, err := sprites.LoadImage(d.Engine, button.Icon)
|
image, err := sprites.LoadImage(d.Engine, button.Icon)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -168,6 +220,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
}
|
}
|
||||||
|
|
||||||
var btnSize = btn.BoxThickness(2) + toolbarSpriteSize
|
var btnSize = btn.BoxThickness(2) + toolbarSpriteSize
|
||||||
|
btn.SetBorderSize(1)
|
||||||
btn.Resize(render.NewRect(btnSize, btnSize))
|
btn.Resize(render.NewRect(btnSize, btnSize))
|
||||||
|
|
||||||
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
@ -181,7 +234,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Edge: tooltipEdge,
|
Edge: tooltipEdge,
|
||||||
})
|
})
|
||||||
|
|
||||||
btnFrame.Pack(btn, btnPack)
|
btnRow.Pack(btn, btnPack)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doodad Editor: show the Layers button.
|
// Doodad Editor: show the Layers button.
|
||||||
|
|
84
pkg/native/engine_sdl.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// +build !js
|
||||||
|
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/render/sdl"
|
||||||
|
sdl2 "github.com/veandco/go-sdl2/sdl"
|
||||||
|
"github.com/veandco/go-sdl2/ttf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Native render engine functions (SDL2 edition),
|
||||||
|
// not for JavaScript/WASM yet.
|
||||||
|
|
||||||
|
/*
|
||||||
|
TextToImage takes an SDL2_TTF texture and makes it into a Go image.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The text is made Black & White with a white background on the image.
|
||||||
|
- Drop shadow, stroke, etc. probably not supported.
|
||||||
|
- Returns a non-antialiased image.
|
||||||
|
*/
|
||||||
|
func TextToImage(e render.Engine, text render.Text) (image.Image, error) {
|
||||||
|
// engine, _ := e.(*sdl.Renderer)
|
||||||
|
|
||||||
|
// Make the text black & white for ease of identifying pixels.
|
||||||
|
text.Color = render.Black
|
||||||
|
|
||||||
|
var (
|
||||||
|
// renderer = engine.GetSDL2Renderer()
|
||||||
|
font *ttf.Font
|
||||||
|
surface *sdl2.Surface
|
||||||
|
pixFmt *sdl2.PixelFormat
|
||||||
|
surface2 *sdl2.Surface
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if font, err = sdl.LoadFont(text.FontFilename, text.Size); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if surface, err = font.RenderUTF8Solid(text.Text, sdl.ColorToSDL(text.Color)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer surface.Free()
|
||||||
|
log.Error("surf fmt: %+v", surface.Format)
|
||||||
|
|
||||||
|
// Convert the Surface into a pixelformat that supports the .At(x,y)
|
||||||
|
// function properly, as the one we got above is "Not implemented"
|
||||||
|
if pixFmt, err = sdl2.AllocFormat(sdl2.PIXELFORMAT_RGB888); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if surface2, err = surface.Convert(pixFmt, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer surface2.Free()
|
||||||
|
|
||||||
|
// Read back the pixels.
|
||||||
|
var (
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
w = int(surface2.W)
|
||||||
|
h = int(surface2.H)
|
||||||
|
img = image.NewRGBA(image.Rect(x, y, w, h))
|
||||||
|
)
|
||||||
|
for x = 0; x < w; x++ {
|
||||||
|
for y = 0; y < h; y++ {
|
||||||
|
hue := surface2.At(x, y)
|
||||||
|
img.Set(x, y, hue)
|
||||||
|
// log.Warn("hue: %s", hue)
|
||||||
|
// r, g, b, _ := hue.RGBA()
|
||||||
|
// if r == 0 && g == 0 && b == 0 {
|
||||||
|
// img.Set(x, y, hue)
|
||||||
|
// } else {
|
||||||
|
// img.Set(x, y, color.Transparent)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return img, nil
|
||||||
|
}
|
13
pkg/native/engine_wasm.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// +build js,wasm
|
||||||
|
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TextToImage(e render.Engine, text render.Text) (image.Image, error) {
|
||||||
|
return nil, errors.New("not supported on WASM")
|
||||||
|
}
|
|
@ -2,7 +2,9 @@ package uix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/render/event"
|
"git.kirsle.net/go/render/event"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
|
@ -137,6 +139,30 @@ func (w *Canvas) loopEditable(ev *event.State) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch w.Tool {
|
switch w.Tool {
|
||||||
|
case drawtool.PanTool:
|
||||||
|
// Pan tool = click to pan the level.
|
||||||
|
if ev.Button1 || keybind.MiddleClick(ev) {
|
||||||
|
if !w.scrollDragging {
|
||||||
|
w.scrollDragging = true
|
||||||
|
w.scrollStartAt = shmem.Cursor
|
||||||
|
w.scrollWasAt = w.Scroll
|
||||||
|
} else {
|
||||||
|
delta := shmem.Cursor.Compare(w.scrollStartAt)
|
||||||
|
w.Scroll = w.scrollWasAt
|
||||||
|
w.Scroll.Subtract(delta)
|
||||||
|
|
||||||
|
// TODO: if I don't call this, the user is able to (temporarily!)
|
||||||
|
// pan outside the level boundaries before it snaps-back when they
|
||||||
|
// release. But the normal middle-click to pan code doesn't let
|
||||||
|
// them do this.. investigate why later.
|
||||||
|
w.loopConstrainScroll()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if w.scrollDragging {
|
||||||
|
w.scrollDragging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case drawtool.PencilTool:
|
case drawtool.PencilTool:
|
||||||
// If no swatch is active, do nothing with mouse clicks.
|
// If no swatch is active, do nothing with mouse clicks.
|
||||||
if w.Palette.ActiveSwatch == nil {
|
if w.Palette.ActiveSwatch == nil {
|
||||||
|
@ -253,6 +279,47 @@ func (w *Canvas) loopEditable(ev *event.State) error {
|
||||||
} else {
|
} else {
|
||||||
w.commitStroke(w.Tool, true)
|
w.commitStroke(w.Tool, true)
|
||||||
}
|
}
|
||||||
|
case drawtool.TextTool:
|
||||||
|
// The Text Tool popup should initialize this for us, if somehow not
|
||||||
|
// initialized skip this tool processing.
|
||||||
|
if w.Palette.ActiveSwatch == nil || drawtool.TT.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we need to create the Label?
|
||||||
|
if drawtool.TT.Label == nil {
|
||||||
|
drawtool.TT.Label = ui.NewLabel(ui.Label{
|
||||||
|
Text: drawtool.TT.Message,
|
||||||
|
Font: render.Text{
|
||||||
|
FontFilename: drawtool.TT.Font,
|
||||||
|
Size: drawtool.TT.Size,
|
||||||
|
Color: w.Palette.ActiveSwatch.Color,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we need to update the color of the label?
|
||||||
|
if drawtool.TT.Label.Font.Color != w.Palette.ActiveSwatch.Color {
|
||||||
|
drawtool.TT.Label.Font.Color = w.Palette.ActiveSwatch.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Canvas.presentStrokes() will handle drawing the font preview
|
||||||
|
// at the cursor location while the TextTool is active.
|
||||||
|
|
||||||
|
// On mouse click, commit the text to the drawing.
|
||||||
|
if ev.Button1 {
|
||||||
|
if stroke, err := drawtool.TT.ToStroke(shmem.CurrentRenderEngine, w.Palette.ActiveSwatch.Color, cursor); err != nil {
|
||||||
|
shmem.FlashError("Text Tool error: %s", err)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
w.currentStroke = stroke
|
||||||
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
|
w.commitStroke(drawtool.PencilTool, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.Button1 = false
|
||||||
|
}
|
||||||
|
|
||||||
case drawtool.EraserTool:
|
case drawtool.EraserTool:
|
||||||
// Clicking? Log all the pixels while doing so.
|
// Clicking? Log all the pixels while doing so.
|
||||||
if ev.Button1 {
|
if ev.Button1 {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
|
@ -89,19 +90,22 @@ func (w *Canvas) loopEditorScroll(ev *event.State) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Middle click of the mouse to pan the level.
|
// Middle click of the mouse to pan the level.
|
||||||
if keybind.MiddleClick(ev) {
|
// NOTE: PanTool intercepts both Left and MiddleClick.
|
||||||
if !w.scrollDragging {
|
if w.Tool != drawtool.PanTool {
|
||||||
w.scrollDragging = true
|
if keybind.MiddleClick(ev) {
|
||||||
w.scrollStartAt = shmem.Cursor
|
if !w.scrollDragging {
|
||||||
w.scrollWasAt = w.Scroll
|
w.scrollDragging = true
|
||||||
|
w.scrollStartAt = shmem.Cursor
|
||||||
|
w.scrollWasAt = w.Scroll
|
||||||
|
} else {
|
||||||
|
delta := shmem.Cursor.Compare(w.scrollStartAt)
|
||||||
|
w.Scroll = w.scrollWasAt
|
||||||
|
w.Scroll.Subtract(delta)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delta := shmem.Cursor.Compare(w.scrollStartAt)
|
if w.scrollDragging {
|
||||||
w.Scroll = w.scrollWasAt
|
w.scrollDragging = false
|
||||||
w.Scroll.Subtract(delta)
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if w.scrollDragging {
|
|
||||||
w.scrollDragging = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,6 +143,11 @@ func (w *Canvas) presentStrokes(e render.Engine) {
|
||||||
if w.Tool == drawtool.ActorTool || w.Tool == drawtool.LinkTool {
|
if w.Tool == drawtool.ActorTool || w.Tool == drawtool.LinkTool {
|
||||||
w.presentActorLinks(e)
|
w.presentActorLinks(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text Tool preview.
|
||||||
|
if w.Tool == drawtool.TextTool && drawtool.TT.Label != nil {
|
||||||
|
drawtool.TT.Label.Present(e, shmem.Cursor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// presentActorLinks draws strokes connecting actors together by their links.
|
// presentActorLinks draws strokes connecting actors together by their links.
|
||||||
|
|
|
@ -31,6 +31,8 @@ type Form struct {
|
||||||
// For vertical forms.
|
// For vertical forms.
|
||||||
Vertical bool
|
Vertical bool
|
||||||
LabelWidth int // size of left frame for labels.
|
LabelWidth int // size of left frame for labels.
|
||||||
|
PadY int // spacer between (vertical) forms
|
||||||
|
PadX int
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -61,6 +63,7 @@ type Field struct {
|
||||||
// Variable bindings, the type may infer to be:
|
// Variable bindings, the type may infer to be:
|
||||||
BoolVariable *bool // Checkbox
|
BoolVariable *bool // Checkbox
|
||||||
TextVariable *string // Textbox
|
TextVariable *string // Textbox
|
||||||
|
IntVariable *int // Textbox
|
||||||
Options []Option // Selectbox
|
Options []Option // Selectbox
|
||||||
SelectValue interface{} // Selectbox default choice
|
SelectValue interface{} // Selectbox default choice
|
||||||
|
|
||||||
|
@ -100,6 +103,7 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
|
||||||
into.Pack(frame, ui.Pack{
|
into.Pack(frame, ui.Pack{
|
||||||
Side: ui.N,
|
Side: ui.N,
|
||||||
FillX: true,
|
FillX: true,
|
||||||
|
PadY: form.PadY,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pager row?
|
// Pager row?
|
||||||
|
@ -177,6 +181,35 @@ func (form Form) Create(into *ui.Frame, fields []Field) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Buttons and Text fields (for now).
|
||||||
|
if row.Type == Button || row.Type == Textbox {
|
||||||
|
btn := ui.NewButton("Button", ui.NewLabel(ui.Label{
|
||||||
|
Text: row.Label,
|
||||||
|
Font: row.Font,
|
||||||
|
TextVariable: row.TextVariable,
|
||||||
|
IntVariable: row.IntVariable,
|
||||||
|
}))
|
||||||
|
form.Supervisor.Add(btn)
|
||||||
|
frame.Pack(btn, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
FillX: true,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tooltip? TODO - make nicer.
|
||||||
|
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
|
||||||
|
ui.NewTooltip(btn, row.Tooltip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
if row.OnClick != nil {
|
||||||
|
row.OnClick()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Checkbox?
|
// Checkbox?
|
||||||
if row.Type == Checkbox {
|
if row.Type == Checkbox {
|
||||||
cb := ui.NewCheckbox("Checkbox", row.BoolVariable, ui.NewLabel(ui.Label{
|
cb := ui.NewCheckbox("Checkbox", row.BoolVariable, ui.NewLabel(ui.Label{
|
||||||
|
@ -266,7 +299,7 @@ func (field Field) Infer() Type {
|
||||||
return Selectbox
|
return Selectbox
|
||||||
}
|
}
|
||||||
|
|
||||||
if field.TextVariable != nil {
|
if field.TextVariable != nil || field.IntVariable != nil {
|
||||||
return Textbox
|
return Textbox
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
124
pkg/windows/text_tool.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/assets"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
|
magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form"
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextTool window.
|
||||||
|
type TextTool struct {
|
||||||
|
// Settings passed in by doodle
|
||||||
|
Supervisor *ui.Supervisor
|
||||||
|
Engine render.Engine
|
||||||
|
|
||||||
|
// Callback when font settings are changed.
|
||||||
|
OnChangeSettings func(font string, size int, message string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextToolWindow initializes the window.
|
||||||
|
func NewTextToolWindow(cfg TextTool) *ui.Window {
|
||||||
|
window := ui.NewWindow("Text Tool")
|
||||||
|
window.SetButtons(ui.CloseButton)
|
||||||
|
window.Configure(ui.Config{
|
||||||
|
Width: 330,
|
||||||
|
Height: 170,
|
||||||
|
Background: render.Grey,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Text variables
|
||||||
|
var (
|
||||||
|
currentText = branding.AppName
|
||||||
|
fontName = balance.TextToolDefaultFont
|
||||||
|
fontSize = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get a listing of the available fonts.
|
||||||
|
fonts, _ := assets.AssetDir("assets/fonts")
|
||||||
|
var fontOption = []magicform.Option{}
|
||||||
|
for _, font := range fonts {
|
||||||
|
// Select the first font by default.
|
||||||
|
if fontName == "" {
|
||||||
|
fontName = font
|
||||||
|
}
|
||||||
|
|
||||||
|
fontOption = append(fontOption, magicform.Option{
|
||||||
|
Label: font,
|
||||||
|
Value: font,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the default config out.
|
||||||
|
if cfg.OnChangeSettings != nil {
|
||||||
|
cfg.OnChangeSettings(fontName, fontSize, currentText)
|
||||||
|
}
|
||||||
|
|
||||||
|
form := magicform.Form{
|
||||||
|
Supervisor: cfg.Supervisor,
|
||||||
|
Engine: cfg.Engine,
|
||||||
|
Vertical: true,
|
||||||
|
LabelWidth: 100,
|
||||||
|
PadY: 2,
|
||||||
|
}
|
||||||
|
form.Create(window.ContentFrame(), []magicform.Field{
|
||||||
|
{
|
||||||
|
Label: "Font Face:",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
Options: fontOption,
|
||||||
|
SelectValue: fontName,
|
||||||
|
OnSelect: func(v interface{}) {
|
||||||
|
fontName = v.(string)
|
||||||
|
if cfg.OnChangeSettings != nil {
|
||||||
|
cfg.OnChangeSettings(fontName, fontSize, currentText)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Font Size:",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
IntVariable: &fontSize,
|
||||||
|
OnClick: func() {
|
||||||
|
shmem.Prompt("Enter new font size: ", func(answer string) {
|
||||||
|
if answer != "" {
|
||||||
|
if i, err := strconv.Atoi(answer); err == nil {
|
||||||
|
fontSize = i
|
||||||
|
if cfg.OnChangeSettings != nil {
|
||||||
|
cfg.OnChangeSettings(fontName, fontSize, currentText)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shmem.FlashError("Not a valid font size: %s", answer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Message:",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
TextVariable: ¤tText,
|
||||||
|
OnClick: func() {
|
||||||
|
shmem.Prompt("Enter new message: ", func(answer string) {
|
||||||
|
if answer != "" {
|
||||||
|
currentText = answer
|
||||||
|
if cfg.OnChangeSettings != nil {
|
||||||
|
cfg.OnChangeSettings(fontName, fontSize, currentText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Be sure the Text Tool is selected, and click onto your\n" +
|
||||||
|
"drawing to place this text onto it.",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return window
|
||||||
|
}
|