From 77297fd60de1b774509936a7d8d5edef6b2d0cf2 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 5 Mar 2022 15:31:09 -0800 Subject: [PATCH] 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. --- assets/sprites/actor-tool.png | Bin 687 -> 661 bytes assets/sprites/ellipse-tool.png | Bin 717 -> 662 bytes assets/sprites/eraser-tool.png | Bin 709 -> 668 bytes assets/sprites/line-tool.png | Bin 626 -> 619 bytes assets/sprites/link-tool.png | Bin 679 -> 665 bytes assets/sprites/pan-tool.png | Bin 0 -> 662 bytes assets/sprites/pencil-tool.png | Bin 752 -> 689 bytes assets/sprites/rect-tool.png | Bin 648 -> 637 bytes assets/sprites/text-tool.png | Bin 0 -> 639 bytes pkg/balance/numbers.go | 4 + pkg/drawtool/text_tool.go | 56 ++++++++++++++ pkg/drawtool/tools.go | 4 + pkg/editor_ui.go | 1 + pkg/editor_ui_popups.go | 24 ++++++ pkg/editor_ui_toolbar.go | 79 ++++++++++++++++---- pkg/native/engine_sdl.go | 84 +++++++++++++++++++++ pkg/native/engine_wasm.go | 13 ++++ pkg/uix/canvas_editable.go | 67 +++++++++++++++++ pkg/uix/canvas_scrolling.go | 28 ++++--- pkg/uix/canvas_strokes.go | 5 ++ pkg/uix/magic-form/magic_form.go | 35 ++++++++- pkg/windows/text_tool.go | 124 +++++++++++++++++++++++++++++++ 22 files changed, 498 insertions(+), 26 deletions(-) create mode 100644 assets/sprites/pan-tool.png create mode 100644 assets/sprites/text-tool.png create mode 100644 pkg/drawtool/text_tool.go create mode 100644 pkg/native/engine_sdl.go create mode 100644 pkg/native/engine_wasm.go create mode 100644 pkg/windows/text_tool.go diff --git a/assets/sprites/actor-tool.png b/assets/sprites/actor-tool.png index c7135bb6bf987cc6661a4f28ada30c2ab4a2d2d8..de13aff7773a23aa5290c096ab972271b32a3f02 100644 GIT binary patch delta 592 zcmV-W0Wie@G~Pb?Bk#V%I6n3YVOc#1ft z=?3KsS&ub;&Rd-IN}aXu$zK@B8!O9Pr#Xxy7O?~w5>(VtMg=zFv>K#XNYj48$3Nuy zWpb(HDua<@0afTwTtE09{O;B&Oip-7kpvKWah#74AiN8-8jkaQ>^Q9xAovVi86AJM z3Cw(w-sottBVb?~xVY|U>K<^p14N%{imA9#kfu;GC;;zg^i4Tn@D}J>^LlIVmj&<^u&3H~|L$UmTO`0VscN zNkluR`R#p`w;qDGj3n>Px7WdRlnxMgMC6 delta 618 zcmV-w0+s!h1+N8viBL{Q4GJ0x0000DNk~Le0000W0000W2nGNE0CReJ^Z)<>glR)V zP)S2WAaHVTW@&6?001bFeUUv#!%!53Pt!_8Dh_rKb;wYiEEE-S)G8FALZ}s5bufA9 zA2cx}DK3tJYr(;v#j1mgv#t)Vf*|+<;^OM0=prTFmlRrm#CYNHKF+)6@ZNoZ(5N!a z3MBwdx6Mo{DdzGkV)zwdgb=_G5;C)lSxHL6x4!PFlj<(cv;6!1tX{QXF(4ok&oINZ ziPwpzHf@9RK5>+lWtI4xc+8{=55U@6iEVIFOKsu0)%&gX5DeVj~%CZ0tBCdE4}UC zXaLinq}SV8>H#Q! zheXvKa_~WdPK$h8Z$|o&ai$p=SovSQaxx%SMEf^SRRati8lT8krYgOdj|G z*t-#7hE8~a*u1}d@i6TTz}bkA08A4d3Qt)O3Wc0?03fClD#A<6FaQ7m07*qoM6N<$ Ef|&&vQ~&?~ diff --git a/assets/sprites/ellipse-tool.png b/assets/sprites/ellipse-tool.png index 044de1231580b81c0766dc018b93f97c3b38ca27..2921942a6f17b83760933111b45a827b01802e7c 100644 GIT binary patch delta 593 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 delta 610 zcmV-o0-gPt1*ZjniBL{Q4GJ0x0000DNk~Le0000W0000W2nGNE0CReJ^Z)<>glR)V zP)S2WAaHVTW@&6?001bFeUUv#!%!53Pt!_8Dh_rKb;wYiEEE-S)G8FALZ}s5bufA9 zA2cx}DK3tJYr(;v#j1mgv#t)Vf*|+<;^OM0=prTFmlRrm#CYNHKF+)6@ZNoZ(5N!a z3MBwdx6Mo{DdzGkV)zwdgb=_G5;C)lSxHL6x4!PFlj<(cv;6!1tX{QXF(4ok&oINZ ziPwpzHf@9RK5>+lWtI4xc+8{=55U@6iEVIFOKsu0)%&gX5DeVj~%CZ0tBCdE4}UC zXaLinq}SV8>H#Q! ze@R3^R9M69RzVKHAPkbm|9_dCcUWSlP!sLlLTEr*JAGin;TLvPD{u)MNdVOZUXJK4 zySYw)Y_^12VmAS1Q?{GHq5cc_cPA>rR}kRrE>wcgJme}BQ3*bp15|-$gNOqA4lSax wM~2z9OB8UnX>UDZ#R+031>~07Qh_@a5B$p$G9|%&1ONa407*qoM6N<$f}#Wt8vpEX>4Tx04R}tkv&MmKpe$iQ%glE4rVCgkfAzR5EXIMDionYs1;guFuC*#nzSS- zE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JWDYS_3;J6>}?mh0_0YbCFbgO3^&<)#6 zClgXOwJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_It$8@RacXzCttxdTL>YKp12Qjn%lC;;zg^i4Tn@D}J>^LlIVSy5gN3fFXh2obQE{apAJ6SB^a|(M$O}mTkPJxH3BhY`8 wJfsYtA6a~89=z(JmhXB*;4DRE`;gnQ0mtbf{Z&lgEC2ui07*qoM6N<$f^*>)Q2+n{ literal 0 HcmV?d00001 diff --git a/assets/sprites/pencil-tool.png b/assets/sprites/pencil-tool.png index 8dd76f6ea49b05917293431fc6a0fefe30dcda30..a96503aab7d911e187a7ddf30dd24f2fdc981270 100644 GIT binary patch delta 620 zcmV-y0+apl1+fKxiBL{Q4GJ0x0000DNk~Le0000O0000O2nGNE0N{5$_y7O_gK0xU zP)S2WAaHVTW@&6?001bFeUUv#!$2IxUsFp(Dh_5S;*g;_Sr8R*)G8FALZ}s5buhW~ z3!1bfDK3tJYr(;f#j1mgv#t)Vf*|+-;^^e0=prTlFDbNti1FaKAMfrx?%n}Hv%++% zXB^NC+e{}DQZ~0LgWie@G~Pb?Bk#V%I6n3YVOc#1ft z=?3KsS&ub;&Rd-IN}aXu$zK@B8!O9Pr#Xxy7O?~w5>(VtMg=zFv>K#XNYj48$3Nuy zWpb(HDua<@0afTwTtE09{O;B&Oip-7kpvKWah#74AiN8-8jkaQ>^Q9xAovVi86AJM z3Cw(w-sottBVb?~xVY|U>K<^p14N%{imA9#kfu;GC;;zg^i4Tn@D}J>^LlIVmj&<^u&3HZaIta;TH*0Vscp zNkl0brQnTvg(LYl6zzqOVNp z3J}6?18m^{#7;N>vJ+mBX$Fq!3{K%kEmASL)R1*kIXmH*9%k+glR)V zP)S2WAaHVTW@&6?001bFeUUv#!%!53Pt!_8Dh_rKb;wYiEEE-S)G8FALZ}s5bufA9 zA2cx}DK3tJYr(;v#j1mgv#t)Vf*|+<;^OM0=prTFmlRrm#CYNHKF+)6@ZNoZ(5N!a z3MBwdx6Mo{DdzGkV)zwdgb=_G5;C)lSxHL6x4!PFlj<(cv;6!1tX{QXF(4ok&oINZ ziPwpzHf@9RK5>+lWtI4xc+8{=55U@6iEVIFOKsu0)%&gX5DeVj~%CZ0tBCdE4}UC zXaLinq}SV8>H#Q! z$Vo&&R9M5smq`x9FbD)`H2?o{&&`^}L0~ZWNQ8^ElZf;l{u(d@M|S|+*dQXJO>jH~ zh32yVwE+WVXU7cCc!t|s;D(C`F$u5Uf(U%sF$8C&4xD7Zjt;`pGmwF&bs$yttQ1$k z*IhaY4?C5^!~JsH!6*a8a6I_pR0pzNWlsswm7aS8VBz2*8bBLdjG;N!_GqwrK`%n< zt&Z#?s|rUYzkFWFOm=$?>rcq~BJ%c-ht)9y@W5HE0lO9;JJs2jQ493*ya7aUHZhv( R3H$&6002ovPDHLkV1hPvIAQ<* diff --git a/assets/sprites/rect-tool.png b/assets/sprites/rect-tool.png index 00afb2c1f02d7db63af80115d8781d782997ea65..c91ace8104a7cdcdeda93a7b197e146e940890d1 100644 GIT binary patch delta 567 zcmV-70?7S{1^onniBL{Q4GJ0x0000DNk~Le0000O0000O2nGNE0N{5$_y7O_gK0xU zP)S2WAaHVTW@&6?001bFeUUv#!$2IxUsFp(Dh_5S;*g;_Sr8R*)G8FALZ}s5buhW~ z3!1bfDK3tJYr(;f#j1mgv#t)Vf*|+-;^^e0=prTlFDbNti1FaKAMfrx?%n}Hv%++% zXB^NC+e{}DQZ~0LgWie@G~Pb?Bk#V%I6n3YVOc#1ft z=?3KsS&ub;&Rd-IN}aXu$zK@B8!O9Pr#Xxy7O?~w5>(VtMg=zFv>K#XNYj48$3Nuy zWpb(HDua<@0afTwTtE09{O;B&Oip-7kpvKWah#74AiN8-8jkaQ>^Q9xAovVi86AJM z3Cw(w-sottBVb?~xVY|U>K<^p14N%{imA9#kfu;GC;;zg^i4Tn@D}J>^LlIVmj&<^u&3HyeHWx~Y@v0Vr!$ zNkl~c3UKei zKBCT}MxSW{5|Dr&0G#B9oX>)bn3lVPsASy&>?0ChR0G}G4KTbzpH%<=002ovPDHLk FV1lS#`QZQn delta 578 zcmV-I0=@nH1c(KHiBL{Q4GJ0x0000DNk~Le0000W0000W2nGNE0CReJ^Z)<>glR)V zP)S2WAaHVTW@&6?001bFeUUv#!%!53Pt!_8Dh_rKb;wYiEEE-S)G8FALZ}s5bufA9 zA2cx}DK3tJYr(;v#j1mgv#t)Vf*|+<;^OM0=prTFmlRrm#CYNHKF+)6@ZNoZ(5N!a z3MBwdx6Mo{DdzGkV)zwdgb=_G5;C)lSxHL6x4!PFlj<(cv;6!1tX{QXF(4ok&oINZ ziPwpzHf@9RK5>+lWtI4xc+8{=55U@6iEVIFOKsu0)%&gX5DeVj~%CZ0tBCdE4}UC zXaLinq}SV8>H#Qq zU`a$lR9M69*1HP8FbqRcxA^~mS=~CN#ifZnY7v3m~F95v1!0`7Gdwab#G ztI=10>mB^tI0JEZe?tHO00000004k|Xe)A6=X@!na-V>^AC>_2D^L(~E(}f$JRa11 QBme*a07*qoM6N<$f^r}Kga7~l diff --git a/assets/sprites/text-tool.png b/assets/sprites/text-tool.png new file mode 100644 index 0000000000000000000000000000000000000000..62d02bc8890f7911e9c14c492992f115fe55647f GIT binary patch literal 639 zcmV-_0)YLAP)EX>4Tx04R}tkv&MmKpe$iQ%glE4rVCgkfAzR5EXIMDionYs1;guFuC*#nzSS- zE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JWDYS_3;J6>}?mh0_0YbCFbgO3^&<)#6 zClgXOwJ}{ee5``6Cn5uTp1mIwF%68lHTZO zu_It$8@RacXzCttxdTL>YKp12Qjn%lC;;zg^i4Tn@D}J>^LlIViS&@`VAm8s8bzfY|Fo`QO&;%)`)N83HPv ZVF0PQ4;xS3;1d7<002ovPDHLkV1m-753~RP literal 0 HcmV?d00001 diff --git a/pkg/balance/numbers.go b/pkg/balance/numbers.go index 5dd85ee..bd2c649 100644 --- a/pkg/balance/numbers.go +++ b/pkg/balance/numbers.go @@ -59,6 +59,10 @@ var ( DefaultEraserBrushSize = 8 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 AutoSaveInterval = 5 * time.Minute diff --git a/pkg/drawtool/text_tool.go b/pkg/drawtool/text_tool.go new file mode 100644 index 0000000..3eca8c1 --- /dev/null +++ b/pkg/drawtool/text_tool.go @@ -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 +} diff --git a/pkg/drawtool/tools.go b/pkg/drawtool/tools.go index 0e0a320..83acd49 100644 --- a/pkg/drawtool/tools.go +++ b/pkg/drawtool/tools.go @@ -12,6 +12,8 @@ const ( ActorTool // drag and move actors LinkTool EraserTool + PanTool + TextTool ) var toolNames = []string{ @@ -22,6 +24,8 @@ var toolNames = []string{ "Doodad", // readable name for ActorTool "Link", "Eraser", + "PanTool", + "TextTool", } func (t Tool) String() string { diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 2005cf5..e33a543 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -51,6 +51,7 @@ type EditorUI struct { doodadWindow *ui.Window paletteEditor *ui.Window layersWindow *ui.Window + textToolWindow *ui.Window publishWindow *ui.Window filesystemWindow *ui.Window licenseWindow *ui.Window diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 440cb6a..f5d935e 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -7,6 +7,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "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/license" "git.kirsle.net/apps/doodle/pkg/log" @@ -49,6 +50,12 @@ func (u *EditorUI) OpenDoodadDropper() { 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. func (u *EditorUI) OpenPublishWindow() { scene, _ := u.d.Scene.(*EditorScene) @@ -148,6 +155,23 @@ func (u *EditorUI) SetupPopups(d *Doodle) { 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 if u.levelSettingsWindow == nil { scene, _ := d.Scene.(*EditorScene) diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index 4679a22..f7dce0c 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -1,6 +1,8 @@ package doodle import ( + "fmt" + "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/enum" @@ -11,33 +13,54 @@ import ( "git.kirsle.net/go/ui/style" ) -// Width of the toolbar frame. -var toolbarWidth = 44 // 38px button (32px sprite + borders) + padding -var toolbarSpriteSize = 32 // 32x32 sprites. +// Global toolbarWidth, TODO: editor_ui.go wants it +var toolbarWidth int // SetupToolbar configures the UI for the Tools panel. func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { // Horizontal toolbar instead of vertical? var ( - isHoz = usercfg.Current.HorizontalToolbars - packAlign = ui.N - frameSize = render.NewRect(toolbarWidth, 100) - tooltipEdge = ui.Right - btnPack = ui.Pack{ + toolbarSpriteSize = 24 // size of sprite images + frameSize render.Rect + isHoz = usercfg.Current.HorizontalToolbars + buttonsPerRow = 2 + packAlign = ui.N + tooltipEdge = ui.Right + btnRowPack = ui.Pack{ Side: packAlign, - PadY: 2, + PadY: 1, + Fill: true, + } + btnPack = ui.Pack{ + Side: ui.W, + PadX: 1, } ) if isHoz { packAlign = ui.W - frameSize = render.NewRect(100, toolbarWidth) tooltipEdge = ui.Bottom - btnPack = ui.Pack{ + btnRowPack = ui.Pack{ Side: packAlign, 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.Resize(frameSize) frame.Configure(ui.Config{ @@ -62,6 +85,16 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { // Optional fields. 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(), 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(), 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 if button.NoDoodad && u.Scene.DrawingType == enum.DoodadDrawing { 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) if err != nil { panic(err) @@ -168,6 +220,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { } var btnSize = btn.BoxThickness(2) + toolbarSpriteSize + btn.SetBorderSize(1) btn.Resize(render.NewRect(btnSize, btnSize)) btn.Handle(ui.Click, func(ed ui.EventData) error { @@ -181,7 +234,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Edge: tooltipEdge, }) - btnFrame.Pack(btn, btnPack) + btnRow.Pack(btn, btnPack) } // Doodad Editor: show the Layers button. diff --git a/pkg/native/engine_sdl.go b/pkg/native/engine_sdl.go new file mode 100644 index 0000000..b3adbbc --- /dev/null +++ b/pkg/native/engine_sdl.go @@ -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 +} diff --git a/pkg/native/engine_wasm.go b/pkg/native/engine_wasm.go new file mode 100644 index 0000000..5fecec5 --- /dev/null +++ b/pkg/native/engine_wasm.go @@ -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") +} diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index f72f519..4004f33 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -2,7 +2,9 @@ package uix import ( "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/shmem" "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" "git.kirsle.net/go/ui" @@ -137,6 +139,30 @@ func (w *Canvas) loopEditable(ev *event.State) error { } 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: // If no swatch is active, do nothing with mouse clicks. if w.Palette.ActiveSwatch == nil { @@ -253,6 +279,47 @@ func (w *Canvas) loopEditable(ev *event.State) error { } else { 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: // Clicking? Log all the pixels while doing so. if ev.Button1 { diff --git a/pkg/uix/canvas_scrolling.go b/pkg/uix/canvas_scrolling.go index 744800d..ebf43f4 100644 --- a/pkg/uix/canvas_scrolling.go +++ b/pkg/uix/canvas_scrolling.go @@ -5,6 +5,7 @@ import ( "fmt" "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/level" "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. - if keybind.MiddleClick(ev) { - if !w.scrollDragging { - w.scrollDragging = true - w.scrollStartAt = shmem.Cursor - w.scrollWasAt = w.Scroll + // NOTE: PanTool intercepts both Left and MiddleClick. + if w.Tool != drawtool.PanTool { + if 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) + } } else { - delta := shmem.Cursor.Compare(w.scrollStartAt) - w.Scroll = w.scrollWasAt - w.Scroll.Subtract(delta) - } - } else { - if w.scrollDragging { - w.scrollDragging = false + if w.scrollDragging { + w.scrollDragging = false + } } } diff --git a/pkg/uix/canvas_strokes.go b/pkg/uix/canvas_strokes.go index 350b415..c6a3a25 100644 --- a/pkg/uix/canvas_strokes.go +++ b/pkg/uix/canvas_strokes.go @@ -143,6 +143,11 @@ func (w *Canvas) presentStrokes(e render.Engine) { if w.Tool == drawtool.ActorTool || w.Tool == drawtool.LinkTool { 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. diff --git a/pkg/uix/magic-form/magic_form.go b/pkg/uix/magic-form/magic_form.go index a652f29..74c54d0 100644 --- a/pkg/uix/magic-form/magic_form.go +++ b/pkg/uix/magic-form/magic_form.go @@ -31,6 +31,8 @@ type Form struct { // For vertical forms. Vertical bool 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: BoolVariable *bool // Checkbox TextVariable *string // Textbox + IntVariable *int // Textbox Options []Option // Selectbox SelectValue interface{} // Selectbox default choice @@ -100,6 +103,7 @@ func (form Form) Create(into *ui.Frame, fields []Field) { into.Pack(frame, ui.Pack{ Side: ui.N, FillX: true, + PadY: form.PadY, }) // 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? if row.Type == Checkbox { cb := ui.NewCheckbox("Checkbox", row.BoolVariable, ui.NewLabel(ui.Label{ @@ -266,7 +299,7 @@ func (field Field) Infer() Type { return Selectbox } - if field.TextVariable != nil { + if field.TextVariable != nil || field.IntVariable != nil { return Textbox } diff --git a/pkg/windows/text_tool.go b/pkg/windows/text_tool.go new file mode 100644 index 0000000..6237d87 --- /dev/null +++ b/pkg/windows/text_tool.go @@ -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 +}