diff --git a/TODO.md b/TODO.md index 77fa3dd..8069555 100644 --- a/TODO.md +++ b/TODO.md @@ -13,4 +13,4 @@ - [X] Add audio settings (music & sound volume). - [X] Hearts must be saved as well for resume. - [ ] Add demo mode. -- [ ] Add touch controls +- [X] Add touch controls diff --git a/cmd/tins2021/app.go b/cmd/tins2021/app.go index 488ecc3..befba3b 100644 --- a/cmd/tins2021/app.go +++ b/cmd/tins2021/app.go @@ -3,6 +3,7 @@ package main import ( "log" + "opslag.de/schobers/geom" "opslag.de/schobers/tins2021" "opslag.de/schobers/zntg/play" "opslag.de/schobers/zntg/ui" @@ -58,9 +59,20 @@ func (a *app) Init(ctx ui.Context) error { ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false) - a.context = newAppContext(ctx, a.settings, a.score, func(control ui.Control) { - a.Content = control + virtual := NewVirtualControls(nil) + virtual.RegisterKey("confirm", ui.KeyEnter, tins2021.MustCreateNamedTextureImage(textures, "return-key", drawReturnKey())) + virtual.RegisterKey("arrow-top-left", ui.KeyUp, tins2021.MustCreateNamedTextureImage(textures, "arrow-top-left-key", drawArrowKey(.75*geom.Pi))) + virtual.RegisterKey("arrow-top-right", ui.KeyRight, tins2021.MustCreateNamedTextureImage(textures, "arrow-top-right-key", drawArrowKey(.25*geom.Pi))) + virtual.RegisterKey("arrow-bottom-left", ui.KeyDown, tins2021.MustCreateNamedTextureImage(textures, "arrow-bottom-left-key", drawArrowKey(1.25*geom.Pi))) + virtual.RegisterKey("arrow-bottom-right", ui.KeyLeft, tins2021.MustCreateNamedTextureImage(textures, "arrow-bottom-right-key", drawArrowKey(1.75*geom.Pi))) + virtual.RegisterKey("cancel", ui.KeyEscape, tins2021.MustCreateNamedTextureImage(textures, "arrow-left-key", drawArrowKey(geom.Pi))) + + virtual.Enabled = a.settings.Controls.Virtual + a.Content = virtual + a.context = newAppContext(ctx, a.settings, a.score, func(view ui.Control) { + virtual.Content = view }) + a.context.Virtual = virtual a.context.ShowMainMenu(ctx) if err := a.context.Audio.LoadSample( diff --git a/cmd/tins2021/appcontext.go b/cmd/tins2021/appcontext.go index a2cf0c6..684d948 100644 --- a/cmd/tins2021/appcontext.go +++ b/cmd/tins2021/appcontext.go @@ -72,6 +72,7 @@ func (app *appContext) Play(ctx ui.Context) { app.ResetCurrentScore() app.show(ctx, newLevelControl(app, ctx, level)) + app.SetVirtualKeys(true, true, true) } func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) { @@ -84,6 +85,7 @@ func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) { level.Score = score level.Lives = lives + app.SetVirtualKeys(true, true, true) controller.Play(level) } @@ -121,6 +123,7 @@ func (app *appContext) PlayResume(ctx ui.Context) { level.Randomize(app.Score.Current.Difficulty, numberOfStars) app.show(ctx, newLevelControl(app, ctx, level)) + app.SetVirtualKeys(true, true, true) } func (app *appContext) ResetCurrentScore() { @@ -144,22 +147,27 @@ func (app *appContext) show(ctx ui.Context, control ui.Control) { func (app *appContext) ShowCredits(ctx ui.Context) { app.show(ctx, newCredits(app, ctx)) + app.SetVirtualKeys(true, true, false) } func (app *appContext) ShowSettings(ctx ui.Context) { app.show(ctx, newSettings(app, ctx)) + app.SetVirtualKeys(true, true, false) } func (app *appContext) ShowHighscores(ctx ui.Context) { app.show(ctx, newHighscores(app, ctx)) + app.SetVirtualKeys(true, true, false) } func (app *appContext) ShowInfo(ctx ui.Context) { app.show(ctx, newInfo(app, ctx)) + app.SetVirtualKeys(true, true, false) } func (app *appContext) ShowMainMenu(ctx ui.Context) { app.show(ctx, newMainMenu(app, ctx)) + app.SetVirtualKeys(false, false, false) } func (app *appContext) setMusicVolume(volume float64) { @@ -180,6 +188,21 @@ func (app *appContext) setSoundVolume(volume float64) { app.Audio.SampleVolume = volume } +func (app *appContext) SetVirtualKeys(cancel, confirm, controls bool) { + var topLeft, topRight, bottomRight, bottomLeft []string + if cancel { + topLeft = append(topLeft, "cancel") + } + if confirm { + topRight = append(topRight, "confirm") + } + if controls { + bottomRight = append(bottomRight, "arrow-top-right", "arrow-bottom-right") + bottomLeft = append(bottomLeft, "arrow-top-left", "arrow-bottom-left") + } + app.Virtual.SetControls(topLeft, topRight, bottomRight, bottomLeft) +} + func (app *appContext) switchToPlayMusic(ctx ui.Context) { app.playNextGameMusic(ctx) menuMusic := app.MenuMusic diff --git a/cmd/tins2021/credits.go b/cmd/tins2021/credits.go index c7a9d00..218e260 100644 --- a/cmd/tins2021/credits.go +++ b/cmd/tins2021/credits.go @@ -169,7 +169,8 @@ func (c *credits) openBrowser(url string) error { func (c *credits) Render(ctx ui.Context) { renderer := ctx.Renderer() - width := c.Bounds().Dx() + bounds := c.Bounds() + width := bounds.Dx() defaultColor := ctx.Style().Palette.Text c.enumerateContent(ctx, func(s string, i int, top, height float32, font ui.Font) { color := defaultColor @@ -186,6 +187,6 @@ func (c *credits) Render(ctx ui.Context) { } textWidth := font.WidthOf(s) - renderer.Text(font, geom.PtF32(.5*(width-textWidth), top), color, s) + renderer.Text(font, geom.PtF32(bounds.Min.X+.5*(width-textWidth), top), color, s) }) } diff --git a/cmd/tins2021/levelcontroller.go b/cmd/tins2021/levelcontroller.go index c076af2..39dccf3 100644 --- a/cmd/tins2021/levelcontroller.go +++ b/cmd/tins2021/levelcontroller.go @@ -102,6 +102,10 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool { switch e := e.(type) { case *ui.KeyDownEvent: switch e.Key { + case ui.KeyEnter: + if r.Level.GameOver { + r.app.ShowHighscores(ctx) + } case ui.KeyEscape: if r.Level.StarsCollected == r.Level.Stars { r.app.SetCurrentScore(r.Level) @@ -128,6 +132,8 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool { checkGameOver := func() { if r.Level.GameOver { + r.app.SetVirtualKeys(true, true, false) + r.Highscore = r.updateHighscore() if r.Highscore { r.app.Audio.PlaySample("level_new_high_score.mp3") @@ -137,6 +143,13 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool { } } + checkLevelCompleted := func() { + if r.Level.StarsCollected == r.Level.Stars { + r.app.SetVirtualKeys(true, true, false) + r.app.Audio.PlaySample("level_completed.mp3") + } + } + monsterHit := func(hit *tins2021.MonsterHit) { r.app.Audio.PlaySample("player_hurt.mp3") if hit == nil { @@ -162,9 +175,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool { } r.app.Audio.PlaySample("player_move.mp3") checkGameOver() - if r.Level.StarsCollected == r.Level.Stars { - r.app.Audio.PlaySample("level_completed.mp3") - } + checkLevelCompleted() } } @@ -252,7 +263,8 @@ func (r levelController) Render(ctx ui.Context) { centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth)) delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y) - view := r.Bounds().Size() + bounds := r.Bounds() + view := bounds.Size() levelView := geom.PtF32(float32(r.Level.Bounds.Dx()+2)*delta.X, float32(r.Level.Bounds.Dy()+2)*delta.Y) textureWidth := geom.Min32( geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)), @@ -262,15 +274,15 @@ func (r levelController) Render(ctx ui.Context) { delta = delta.Mul(textureWidth) centerTopSquare = centerTopSquare.Mul(textureWidth) - scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY) + scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY).Add(bounds.Min) delta.X = geom.Round32(delta.X) delta.Y = geom.Round32(delta.Y) toScreen := func(p geom.Point) geom.PointF32 { if p.Y%2 == 0 { - return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY) + return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY).Add(bounds.Min) } - return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY) + return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY).Add(bounds.Min) } renderer := ctx.Renderer() @@ -404,12 +416,13 @@ func (r levelController) Render(ctx ui.Context) { scoreTopLeft.Y -= scoreFont.Height() renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:") - bounds := r.Bounds() - centerX := .5 * bounds.Dx() + centerX := bounds.Min.X + .5*bounds.Dx() titleFont := ctx.Fonts().Font("title") + screenSize := ctx.Renderer().Size().ToF32() + screen := geom.RectF32(0, 0, screenSize.X, screenSize.Y) if r.Level.GameOver { - renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`)) + renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`)) offsetY := .5*bounds.Dy() - titleFont.Height() renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter) @@ -422,12 +435,15 @@ func (r levelController) Render(ctx ui.Context) { offsetY += titleFont.Height() + scoreFont.Height() renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Final score: %d", r.Level.Score), ui.AlignCenter) + offsetY += 2 * scoreFont.Height() + renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [enter] to show highscores.", ui.AlignCenter) + offsetY += 2 * scoreFont.Height() renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter) } else if r.Level.StarsCollected == r.Level.Stars { - renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`)) + renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`)) offsetY := .5*bounds.Dy() - titleFont.Height() - renderer.TextAlign(titleFont, geom.PtF32(.5*bounds.Dx(), offsetY), textColor, "COMPLETED", ui.AlignCenter) + renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "COMPLETED", ui.AlignCenter) offsetY += titleFont.Height() + scoreFont.Height() renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter) diff --git a/cmd/tins2021/menu.go b/cmd/tins2021/menu.go index baceea3..a45ae09 100644 --- a/cmd/tins2021/menu.go +++ b/cmd/tins2021/menu.go @@ -66,6 +66,7 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool { for i, button := range m.buttons { if button.IsOver() { m.updateActiveButton(ctx, i) + ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer) break } } diff --git a/cmd/tins2021/settings.go b/cmd/tins2021/settings.go index 67f07de..4c1a7c0 100644 --- a/cmd/tins2021/settings.go +++ b/cmd/tins2021/settings.go @@ -36,14 +36,31 @@ const volumeControlTextureHeight = 256 const volumeControlTextureWidth = 2*volumeControlKnobWidth + 2*volumeControlBarWidth func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.PointF, key rune, color color.Color) { - const cornerRadius = keyboardKeyCornerRadius const keyHeight_5 = .5 * keyboardKeyHeight - const keyWidth_5 = .5 * keyboardKeyWidth skewed := func(x, y float64) (float64, float64) { - x, y = skewedKeyboardCoordinates(x, y) - return center.X + x, center.Y + y + return center.X + x - keyboardKeySkew*y, center.Y + y } + drawKeyOutline(ctx, center, geom.PtF(keyboardKeyWidth, keyboardKeyHeight), keyboardKeySkew, color) + + setDraw2DFont(ctx, font) + ctx.SetFontSize(keyHeight_5) + + text := fmt.Sprintf("%c", key) + textCenter := draw2DCenterString(ctx, text) + textX, textY := skewed(textCenter.X, textCenter.Y) + ctx.FillStringAt(text, textX, textY) +} + +func drawKeyOutline(ctx *draw2dimg.GraphicContext, center, size geom.PointF, skew float64, color color.Color) { + const cornerRadius = keyboardKeyCornerRadius + var keyHeight_5 = .5 * size.Y + var keyWidth_5 = .5 * size.X + + skewed := func(x, y float64) (float64, float64) { + return center.X + x - skew*y, center.Y + y + } + corner := func(x, y, start float64) { for a := start; a <= start+.25; a += .025 { aa := a * 2 * geom.Pi @@ -65,14 +82,6 @@ func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.Poi corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5) ctx.Close() ctx.Stroke() - - setDraw2DFont(ctx, font) - ctx.SetFontSize(keyHeight_5) - - text := fmt.Sprintf("%c", key) - textCenter := draw2DCenterString(ctx, text) - textX, textY := skewed(textCenter.X, textCenter.Y) - ctx.FillStringAt(text, textX, textY) } func generateArrowKeys(resources ui.Resources) image.Image { @@ -210,7 +219,8 @@ type keyboardLayoutKey struct { type keyboardLayoutSettings struct { ui.ControlBase - app *appContext + app *appContext + handler SettingHandler Active bool ActiveLayout int @@ -219,7 +229,7 @@ type keyboardLayoutSettings struct { SelectingCustom int } -func newKeyboardLayoutSettings(app *appContext, ctx ui.Context) *keyboardLayoutSettings { +func newKeyboardLayoutSettings(app *appContext, ctx ui.Context, handler SettingHandler) *keyboardLayoutSettings { ctx.Textures().CreateTextureGo("layout-wasd", generateWASDKeys(ctx.Resources()), true) ctx.Textures().CreateTextureGo("layout-arrows", generateArrowKeys(ctx.Resources()), true) ctx.Textures().CreateTextureGo("layout-select-1", generateArrowKeysHighlight(ctx.Resources(), [4]bool{true, false, false, false}), true) @@ -235,7 +245,7 @@ func newKeyboardLayoutSettings(app *appContext, ctx ui.Context) *keyboardLayoutS layout = 2 } - settings := &keyboardLayoutSettings{app: app, ActiveLayout: layout, SelectedLayout: layout} + settings := &keyboardLayoutSettings{app: app, handler: handler, ActiveLayout: layout, SelectedLayout: layout} settings.renderCustomLayout(ctx) return settings } @@ -307,6 +317,8 @@ func (s *keyboardLayoutSettings) Handle(ctx ui.Context, e ui.Event) bool { layout := s.isOverLayout(ctx, e.Pos()) if layout > -1 { s.setActiveLayout(layout) + s.handler.Activated(ctx) + ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer) } } s.Active = s.IsOver() @@ -324,9 +336,12 @@ func (s *keyboardLayoutSettings) Handle(ctx ui.Context, e ui.Event) bool { return false } +func (s *keyboardLayoutSettings) IsActive() bool { return s.Active } + func (s *keyboardLayoutSettings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int { bounds := s.Bounds() width := bounds.Dx() + mouse = mouse.Sub(s.Offset()) scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width) @@ -438,42 +453,87 @@ func (s *keyboardLayoutSettings) selectLayout() { } } +func (s *keyboardLayoutSettings) SetActive(_ ui.Context, active bool) { s.Active = active } + func (s *keyboardLayoutSettings) setActiveLayout(layout int) { layout = (layout + 3) % 3 change := layout != s.ActiveLayout + s.Active = true s.ActiveLayout = (layout + 3) % 3 if change { s.app.MenuInteraction() } } +type Setting interface { + IsActive() bool + SetActive(ui.Context, bool) +} + +type SettingHandler interface { + Interacted() + Activated(ui.Context) +} + +type settingHandler struct { + app *appContext + settings *settings + + Setting int +} + +func (h *settingHandler) Interacted() { + h.app.MenuInteraction() +} + +func (h *settingHandler) Activated(ctx ui.Context) { + h.settings.setActive(ctx, h.Setting, false) +} + type settings struct { ui.Proxy + active int app *appContext + overflow ui.ScrollControl musicVolume *volumeControl soundVolume *volumeControl + virtual *toggleControl keyboard *keyboardLayoutSettings } func newSettings(app *appContext, ctx ui.Context) *settings { volumeControlTexture := generateTextureMapFromImage(ctx.Textures(), "volume-control", generateVolumeControlTexture) - settings := &settings{app: app, - musicVolume: newVolumeControl(app, volumeControlTexture, "Music", app.Settings.Audio.MusicVolume, func(volume float64) { app.setMusicVolume(volume) }), - soundVolume: newVolumeControl(app, volumeControlTexture, "Sounds", app.Settings.Audio.SoundVolume, func(volume float64) { app.setSoundVolume(volume) }), - keyboard: newKeyboardLayoutSettings(app, ctx), + settings := &settings{app: app} + var s int + handler := func() *settingHandler { + defer func() { s++ }() + return &settingHandler{Setting: s, settings: settings, app: app} } - settings.Content = ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { + settings.musicVolume = newVolumeControl(handler(), volumeControlTexture, "Music", app.Settings.Audio.MusicVolume, func(volume float64) { app.setMusicVolume(volume) }) + settings.soundVolume = newVolumeControl(handler(), volumeControlTexture, "Sounds", app.Settings.Audio.SoundVolume, func(volume float64) { app.setSoundVolume(volume) }) + settings.virtual = newToggleControl(handler(), "VIRTUAL CONTROLS", app.Settings.Controls.Virtual, func(on bool) { + app.Settings.Controls.Virtual = on + app.Virtual.Enabled = on + }) + settings.keyboard = newKeyboardLayoutSettings(app, ctx, handler()) + + settings.Content = ui.StretchWidth(ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { p.AddChild(Center(label("SETTINGS", "title"))) p.AddChild(label("", "score")) - p.AddChild(Center(settings.musicVolume)) - p.AddChild(label("", "score")) - p.AddChild(Center(settings.soundVolume)) - p.AddChild(label("", "score")) - p.AddChild(settings.keyboard) - }) + settings.overflow = ui.Overflow(ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { + p.AddChild(Center(settings.musicVolume)) + p.AddChild(label("", "score")) + p.AddChild(Center(settings.soundVolume)) + p.AddChild(label("", "score")) + p.AddChild(Center(settings.virtual)) + p.AddChild(label("", "score")) + p.AddChild(settings.keyboard) + })) + p.AddChild(settings.overflow) + })) settings.musicVolume.Active = true @@ -486,8 +546,10 @@ func (s *settings) currentActive() int { return 0 case s.soundVolume.Active: return 1 - case s.keyboard.Active: + case s.virtual.Active: return 2 + case s.keyboard.Active: + return 3 default: return 0 } @@ -502,9 +564,9 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool { case *ui.KeyDownEvent: switch e.Key { case ui.KeyUp: - s.setActive(s.currentActive() - 1) + s.setActive(ctx, s.currentActive()-1, true) case ui.KeyDown: - s.setActive(s.currentActive() + 1) + s.setActive(ctx, s.currentActive()+1, true) } } @@ -516,11 +578,12 @@ func (s *settings) Render(ctx ui.Context) { s.keyboard.PostRender(ctx) } -func (s *settings) setActive(active int) { - controls := [3]*bool{ - &s.musicVolume.Active, - &s.soundVolume.Active, - &s.keyboard.Active, +func (s *settings) setActive(ctx ui.Context, active int, key bool) { + controls := []Setting{ + s.musicVolume, + s.soundVolume, + s.virtual, + s.keyboard, } for active < 0 { active += len(controls) @@ -528,17 +591,20 @@ func (s *settings) setActive(active int) { if active > 0 { active = active % len(controls) } - s.app.MenuInteraction() + + if s.active != active { + s.active = active + if key { + controls[active].(ui.Control).ScrollIntoView(ctx, nil) + } + s.app.MenuInteraction() + } for i := range controls { - *controls[i] = i == active + controls[i].SetActive(ctx, i == active) } } -func skewedKeyboardCoordinates(x, y float64) (float64, float64) { - return x - keyboardKeySkew*y, y -} - var supportedCustomKeys = map[ui.Key]string{ ui.KeyA: "A", ui.KeyB: "B", @@ -568,10 +634,85 @@ var supportedCustomKeys = map[ui.Key]string{ ui.KeyZ: "Z", } +type toggleControl struct { + ui.StackPanel + + handler SettingHandler + caption *ui.Label + on *ui.Label + changed func(bool) + + Active bool + On bool +} + +func newToggleControl(handler SettingHandler, name string, on bool, changed func(bool)) *toggleControl { + center := func(l *ui.Label) { l.TextAlignment = ui.AlignCenter } + toggle := &toggleControl{handler: handler, On: on, caption: ui.BuildLabel(name, center), on: ui.BuildLabel("", center), changed: changed} + toggle.AddChild(toggle.caption) + toggle.AddChild(toggle.on) + toggle.updateOn() + return toggle +} + +func (c *toggleControl) toggle() { + c.On = !c.On + c.updateOn() + c.handler.Interacted() + changed := c.changed + if changed != nil { + changed(c.On) + } +} + +func (c *toggleControl) Handle(ctx ui.Context, e ui.Event) bool { + if c.ControlBase.Handle(ctx, e) { + return true + } + switch e := e.(type) { + case *ui.MouseMoveEvent: + if !c.Active && c.IsOver() { + c.Active = true + c.handler.Activated(ctx) + } + if c.IsOver() { + ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer) + } + case *ui.MouseButtonDownEvent: + if e.Button == ui.MouseButtonLeft && c.Active && c.IsOver() { + c.toggle() + } + case *ui.KeyDownEvent: + if c.Active && e.Key == ui.KeyEnter { + c.toggle() + } + } + return false +} + +func (c *toggleControl) IsActive() bool { return c.Active } + +func (c *toggleControl) SetActive(ctx ui.Context, active bool) { + c.Active = active + if active { + c.caption.Font.Color = ctx.Style().Palette.Primary + } else { + c.caption.Font.Color = ctx.Style().Palette.Text + } +} + +func (c *toggleControl) updateOn() { + if c.On { + c.on.Text = "ON" + } else { + c.on.Text = "OFF" + } +} + type volumeControl struct { ui.ControlBase - app *appContext + handler SettingHandler texture *TextureMap Active bool @@ -584,8 +725,8 @@ type volumeControl struct { VolumeChanged func(float64) } -func newVolumeControl(app *appContext, texture *TextureMap, name string, volume float64, changed func(float64)) *volumeControl { - control := &volumeControl{app: app, texture: texture, overBar: -1, Name: name, Volume: volume, VolumeChanged: changed} +func newVolumeControl(handler SettingHandler, texture *TextureMap, name string, volume float64, changed func(float64)) *volumeControl { + control := &volumeControl{handler: handler, texture: texture, overBar: -1, Name: name, Volume: volume, VolumeChanged: changed} return control } @@ -593,16 +734,6 @@ func (c *volumeControl) changeVolume(delta float64) { c.setVolume(c.Volume + delta) } -func (c *volumeControl) setVolume(volume float64) { - volume = geom.Min(maxVolume, geom.Max(minVolume, volume)) - if c.Volume == volume { - return - } - c.Volume = volume - c.VolumeChanged(c.Volume) - c.app.MenuInteraction() -} - func (c *volumeControl) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { font := ctx.Fonts().Font("default") return geom.PtF32(geom.NaN32(), 2.5*font.Height()) @@ -633,27 +764,34 @@ func (c *volumeControl) Handle(ctx ui.Context, e ui.Event) bool { c.overRightKnob = false case 0: if !c.overLeftKnob { - c.app.MenuInteraction() + c.handler.Interacted() } c.overLeftKnob = true c.overBar = -1 c.overRightKnob = false case 11: if !c.overRightKnob { - c.app.MenuInteraction() + c.handler.Interacted() } c.overLeftKnob = false c.overBar = -1 c.overRightKnob = true default: if over-1 != c.overBar { - c.app.MenuInteraction() + c.handler.Interacted() } c.overLeftKnob = false c.overBar = over - 1 c.overRightKnob = false } + if over > -1 { + ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer) + } + active := c.Active c.Active = c.IsOver() + if !active && c.Active { + c.handler.Activated(ctx) + } case *ui.MouseButtonDownEvent: if e.Button == ui.MouseButtonLeft { over := c.isOver(ctx, e.Pos()) @@ -671,8 +809,11 @@ func (c *volumeControl) Handle(ctx ui.Context, e ui.Event) bool { return false } +func (c *volumeControl) IsActive() bool { return c.Active } + func (c *volumeControl) isOver(ctx ui.Context, p geom.PointF32) int { bounds := c.Bounds() + p = p.Sub(c.Offset()) font := ctx.Fonts().Font("default") top := bounds.Min.Y + 1.5*font.Height() @@ -744,3 +885,15 @@ func (c *volumeControl) Render(ctx ui.Context) { } ctx.Renderer().DrawTexturePointOptions(rightKnob, geom.PtF32(left, top), ui.DrawOptions{Source: &rightKnobRegion, Tint: tint(c.overRightKnob)}) } + +func (c *volumeControl) SetActive(_ ui.Context, active bool) { c.Active = active } + +func (c *volumeControl) setVolume(volume float64) { + volume = geom.Min(maxVolume, geom.Max(minVolume, volume)) + if c.Volume == volume { + return + } + c.Volume = volume + c.VolumeChanged(c.Volume) + c.handler.Interacted() +} diff --git a/cmd/tins2021/virtualcontrols.go b/cmd/tins2021/virtualcontrols.go new file mode 100644 index 0000000..04b4c9e --- /dev/null +++ b/cmd/tins2021/virtualcontrols.go @@ -0,0 +1,237 @@ +package main + +import ( + "fmt" + "image" + "image/color" + + "github.com/llgcode/draw2d" + "github.com/llgcode/draw2d/draw2dimg" + "opslag.de/schobers/geom" + "opslag.de/schobers/geom/ints" + "opslag.de/schobers/tins2021" + "opslag.de/schobers/zntg/ui" +) + +func drawArrowKey(a float64) image.Image { + const keyHeight_5 = .5 * tins2021.TextureSize + const keyWidth_5 = .5 * tins2021.TextureSize + + im := image.NewRGBA(image.Rect(0, 0, tins2021.TextureSize, tins2021.TextureSize)) + ctx := draw2dimg.NewGraphicContext(im) + center := geom.PtF(keyWidth_5, keyHeight_5) + drawKeyOutline(ctx, center, geom.PtF(.95*tins2021.TextureSize, .76*tins2021.TextureSize), 0, color.White) + point := center.Add(tins2021.Polar(a, .25*tins2021.TextureSize)) + back := center.Add(tins2021.Polar(a+geom.Pi, .25*tins2021.TextureSize)) + + ctx.SetLineWidth(3) + ctx.SetStrokeColor(color.White) + ctx.SetLineCap(draw2d.RoundCap) + + ctx.MoveTo(back.XY()) + ctx.LineTo(point.XY()) + ctx.LineTo(point.Add(tins2021.Polar(a+.75*geom.Pi, .25*tins2021.TextureSize)).XY()) + ctx.Stroke() + ctx.MoveTo(point.XY()) + ctx.LineTo(point.Add(tins2021.Polar(a-.75*geom.Pi, .25*tins2021.TextureSize)).XY()) + ctx.Stroke() + + return im +} + +func drawReturnKey() image.Image { + const keyHeight_5 = .5 * tins2021.TextureSize + const keyWidth_5 = .5 * tins2021.TextureSize + + im := image.NewRGBA(image.Rect(0, 0, tins2021.TextureSize, tins2021.TextureSize)) + ctx := draw2dimg.NewGraphicContext(im) + drawKeyOutline(ctx, geom.PtF(keyWidth_5, keyHeight_5), geom.PtF(.95*tins2021.TextureSize, .76*tins2021.TextureSize), 0, color.White) + + unity := func(f float64) float64 { return f * tins2021.TextureSize } + + ctx.SetLineWidth(3) + ctx.SetStrokeColor(color.White) + ctx.SetLineCap(draw2d.RoundCap) + + ctx.MoveTo(unity(.75), unity(.3)) + ctx.LineTo(unity(.75), unity(.55)) + ctx.LineTo(unity(.25), unity(.55)) + ctx.LineTo(unity(.4), unity(.7)) + ctx.Stroke() + ctx.MoveTo(unity(.25), unity(.55)) + ctx.LineTo(unity(.4), unity(.4)) + ctx.Stroke() + + return im +} + +type VirtualControls struct { + ui.Proxy + + bounds geom.RectangleF32 + + Enabled bool + Keys map[string]VirtualKey + TopLeftControls []string + TopRightControls []string + BottomRightControls []string + BottomLeftControls []string +} + +func NewVirtualControls(content ui.Control) *VirtualControls { + return &VirtualControls{ + Proxy: ui.Proxy{Content: content}, + Keys: map[string]VirtualKey{}, + } +} + +func (c *VirtualControls) allKeys() map[string]geom.RectangleF32 { + bounds := c.bounds + size, _, margin := c.lengths() + + keySize := geom.PtF32(size, size) + keys := map[string]geom.RectangleF32{} + left := float32(margin) + top := bounds.Min.Y + for _, name := range c.TopLeftControls { + keys[name] = geom.PtF32(left, top).RectRel(keySize) + top += size + margin + } + + left = bounds.Max.X - size - margin + top = bounds.Min.Y + for _, name := range c.TopRightControls { + keys[name] = geom.PtF32(left, top).RectRel(keySize) + top += size + margin + } + + left = float32(margin) + top = bounds.Max.Y - size + for _, name := range c.BottomLeftControls { + keys[name] = geom.PtF32(left, top).RectRel(keySize) + top -= size + margin + } + + left = bounds.Max.X - size - margin + top = bounds.Max.Y - size + for _, name := range c.BottomRightControls { + keys[name] = geom.PtF32(left, top).RectRel(keySize) + top -= size + margin + } + return keys +} + +func (c *VirtualControls) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) { + c.bounds = bounds + if c.Enabled { + virtualControlSize, _ := c.desiredVirtualControlSize(bounds.Size()) + bounds.Min.X += virtualControlSize + bounds.Max.X -= virtualControlSize + if bounds.Max.X < bounds.Min.X { + center := .5 * (bounds.Min.X + bounds.Max.X) + bounds.Min.X = center + bounds.Max.X = center + } + } + c.Content.Arrange(ctx, bounds, offset, parent) +} + +func (c *VirtualControls) desiredVirtualControlSize(size geom.PointF32) (float32, int) { + if size.X > 2*size.Y { + return size.Y / 5, 2 + } + return size.Y / 7, 3 +} + +func (c *VirtualControls) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { + desired := c.Content.DesiredSize(ctx, size) + if geom.IsNaN32(desired.X) { + return desired + } + if c.Enabled { + virtualControlSize, rows := c.desiredVirtualControlSize(size) + columns := ints.Max(len(c.TopLeftControls)+rows-1/rows, len(c.BottomLeftControls)+rows-1/rows) + ints.Max(len(c.TopRightControls)+rows-1/rows, len(c.BottomRightControls)+rows-1/rows) + fmt.Println(columns) + desired.X = geom.Min32(desired.X+float32(columns)*virtualControlSize, size.X) + } + return desired +} + +func (c *VirtualControls) Handle(ctx ui.Context, e ui.Event) bool { + if c.Enabled { + switch e := e.(type) { + case *ui.MouseMoveEvent: + pos := e.Pos() + for _, bounds := range c.allKeys() { + if pos.In(bounds) { + ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer) + break + } + } + case *ui.MouseButtonDownEvent: + if e.Button == ui.MouseButtonLeft { + pos := e.Pos() + for name, bounds := range c.allKeys() { + if pos.In(bounds) { + key := c.Keys[name] + c.Content.Handle(ctx, &ui.KeyDownEvent{ + EventBase: ui.EventBase{StampInSeconds: e.StampInSeconds}, + Key: key.Key, + }) + break + } + } + } + } + } + return c.Content.Handle(ctx, e) +} + +func (c *VirtualControls) lengths() (size, scale, margin float32) { + bounds := c.bounds + margin = 8 + size, _ = c.desiredVirtualControlSize(bounds.Size()) + size -= margin + scale = tins2021.FindScaleRound(tins2021.TextureSize, size) + return +} + +func (c *VirtualControls) RegisterKey(id string, key ui.Key, symbol tins2021.NamedTexture) { + c.Keys[id] = VirtualKey{Key: key, Symbol: symbol} +} + +func (c *VirtualControls) Render(ctx ui.Context) { + c.Content.Render(ctx) + + if c.Enabled { + _, scale, _ := c.lengths() + + mouse := ctx.MousePosition() + normalColor := ctx.Style().Palette.Text + overColor := ctx.Style().Palette.Primary + + renderer := ctx.Renderer() + + for name, keyBounds := range c.allKeys() { + key := c.Keys[name] + texture := key.Symbol.Scaled(scale) + color := normalColor + if mouse.In(keyBounds) { + color = overColor + } + renderer.DrawTexturePointOptions(texture, keyBounds.Min, ui.DrawOptions{Tint: color}) + } + } +} + +func (c *VirtualControls) SetControls(topLeft, topRight, bottomRight, bottomLeft []string) { + c.TopLeftControls = topLeft + c.TopRightControls = topRight + c.BottomRightControls = bottomRight + c.BottomLeftControls = bottomLeft +} + +type VirtualKey struct { + Symbol tins2021.NamedTexture + Key ui.Key +} diff --git a/settings.go b/settings.go index 13f6f82..f2f3c12 100644 --- a/settings.go +++ b/settings.go @@ -19,6 +19,7 @@ type ControlsSettings struct { MoveDownLeft string MoveUpLeft string MoveUpRight string + Virtual bool } type Settings struct {