diff --git a/assets b/assets index e72d3737..98b3c0a0 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit e72d3737fb5d35128de1eb72498a8b8acaab52bb +Subproject commit 98b3c0a088856b37dcd78f9a82df7a37ce968051 diff --git a/database b/database index c31e8f12..aad97888 160000 --- a/database +++ b/database @@ -1 +1 @@ -Subproject commit c31e8f126d8c6975ea8ed12b22a3d718a11ee853 +Subproject commit aad97888edaccdb9a37491ccd84453436b9e2e46 diff --git a/go.mod b/go.mod index c9382107..8ade5c63 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mholt/archiver/v3 v3.5.1 github.com/pelletier/go-toml v1.9.5 github.com/tanema/gween v0.0.0-20221212145351-621cc8a459d1 diff --git a/go.sum b/go.sum index 9ba271c3..a440379d 100644 --- a/go.sum +++ b/go.sum @@ -17,15 +17,11 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -34,16 +30,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= @@ -52,8 +44,6 @@ github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWk github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -83,16 +73,10 @@ github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVY github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= -golang.org/x/mobile v0.0.0-20241204233305-ce44b2716d33 h1:MeYjBsMjR6w9aXA3oHEJWCQ5bnD5fOjesYMOn52ovQY= -golang.org/x/mobile v0.0.0-20241204233305-ce44b2716d33/go.mod h1:Sf9LBimL0mWKEdgAjRmJ6iu7Z34osHQTK/devqFbM2I= golang.org/x/mobile v0.0.0-20250305212854-3a7bc9f8a4de h1:WuckfUoaRGJfaQTPZvlmcaQwg4Xj9oS2cvvh3dUqpDo= golang.org/x/mobile v0.0.0-20250305212854-3a7bc9f8a4de/go.mod h1:/IZuixag1ELW37+FftdmIt59/3esqpAWM/QqWtf7HUI= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index 09a1e288..75736d91 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/go-gl/glfw/v3.3/glfw" "github.com/libretro/ludo/audio" "github.com/libretro/ludo/core" + "github.com/libretro/ludo/dat" "github.com/libretro/ludo/history" "github.com/libretro/ludo/input" "github.com/libretro/ludo/menu" @@ -20,6 +21,7 @@ import ( "github.com/libretro/ludo/scanner" "github.com/libretro/ludo/settings" "github.com/libretro/ludo/state" + "github.com/libretro/ludo/utils" "github.com/libretro/ludo/video" ) @@ -40,6 +42,9 @@ func runLoop(vid *video.Video, m *menu.Menu) { m.ProcessHotkeys() ntf.Process(dt) vid.ResizeViewport() + w, h := vid.Window.GetFramebufferSize() + vid.Font.UpdateResolution(w, h) + vid.BoldFont.UpdateResolution(w, h) m.UpdatePalette() input.Poll() if !state.MenuActive { @@ -135,7 +140,20 @@ func main() { if err != nil { ntf.DisplayAndLog(ntf.Error, "Menu", err.Error()) } else { - m.WarpToQuickMenu() + scanner.ScanFile(gamePath, func(game dat.Game) { + name := game.Name + if name == "" { + name = utils.FileName(gamePath) + } + history.Push(history.Game{ + Path: gamePath, + Name: name, + System: game.System, + CorePath: state.CorePath, + }) + history.Load() + m.WarpToQuickMenu() + }) } } } else { diff --git a/menu/hints.go b/menu/hints.go index ace0d6cc..e3d06f90 100644 --- a/menu/hints.go +++ b/menu/hints.go @@ -5,13 +5,23 @@ import ( ) // Used to easily compose different hint bars based on the context. -func stackHint(stack *float32, icon uint32, label string, h int) { - menu.Font.SetColor(darkGrey) - *stack += 30 * menu.ratio - menu.DrawImage(icon, *stack, float32(h)-70*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, darkGrey) +func stackHintLeft(stack *float32, icon uint32, label string, h int) { + menu.Font.SetColor(hintTextColor) + menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor) *stack += 70 * menu.ratio - menu.Font.Printf(*stack, float32(h)-23*menu.ratio, 0.4*menu.ratio, label) - *stack += menu.Font.Width(0.4*menu.ratio, label) + menu.Font.Printf(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label) + *stack += menu.Font.Width(0.5*menu.ratio, label) + *stack += 32 * menu.ratio +} + +// Used to easily compose different hint bars based on the context. +func stackHintRight(stack *float32, icon uint32, label string, h int) { + *stack -= menu.Font.Width(0.5*menu.ratio, label) + menu.Font.SetColor(hintTextColor) + menu.Font.Printf(*stack, float32(h)-30*menu.ratio, 0.5*menu.ratio, label) + *stack -= 70 * menu.ratio + menu.DrawImage(icon, *stack, float32(h)-79*menu.ratio, 70*menu.ratio, 70*menu.ratio, 1.0, 0, hintTextColor) + *stack -= 32 * menu.ratio } func hintIcons() (arrows, upDown, leftRight, a, b, x, y, start, slct, guide uint32) { diff --git a/menu/input.go b/menu/input.go index 28b8c648..17213dc2 100644 --- a/menu/input.go +++ b/menu/input.go @@ -20,7 +20,7 @@ var ( // Update takes care of calling the update method of the current scene. // Each scene has it's own input logic to allow a variety of navigation systems. func (m *Menu) Update(dt float32) { - currentScene := m.stack[len(m.stack)-1] + currentScene := m.stack[m.focus-1] currentScene.update(dt) } @@ -67,22 +67,20 @@ func withRepeat() func(dt float32, pressed bool, f func()) { func genericInput(list *entry, dt float32) { // Down repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() { - list.ptr++ - if list.ptr >= len(list.children) { - list.ptr = 0 + if list.ptr < len(list.children)-1 { + list.ptr++ + audio.PlayEffect(audio.Effects["down"]) + genericAnimate(list) } - audio.PlayEffect(audio.Effects["down"]) - genericAnimate(list) }) // Up repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() { - list.ptr-- - if list.ptr < 0 { - list.ptr = len(list.children) - 1 + if list.ptr > 0 { + list.ptr-- + audio.PlayEffect(audio.Effects["up"]) + genericAnimate(list) } - audio.PlayEffect(audio.Effects["up"]) - genericAnimate(list) }) // OK @@ -122,7 +120,10 @@ func genericInput(list *entry, dt float32) { if len(menu.stack) > 1 { audio.PlayEffect(audio.Effects["cancel"]) menu.stack[len(menu.stack)-2].segueBack() - menu.stack = menu.stack[:len(menu.stack)-1] + if len(menu.stack) > 2 { + menu.stack = menu.stack[:len(menu.stack)-1] + } + menu.focus-- } } diff --git a/menu/menu.go b/menu/menu.go index 192b5f6c..dab826ef 100644 --- a/menu/menu.go +++ b/menu/menu.go @@ -16,12 +16,14 @@ var menu *Menu // Menu is a type holding the menu state, the stack of scenes, tweens, etc type Menu struct { - stack []Scene - icons map[string]uint32 - tweens Tweens - scroll float32 - ratio float32 - t float64 + focus int // this is a hack to switch focus between top tabs and the other scenes + oldFocus int // this is used to come back to the previous focus when a dialog is canceled + stack []Scene + icons map[string]uint32 + tweens Tweens + scroll float32 + ratio float32 + t float64 *video.Video // we embbed video here to have direct access to drawing functions } @@ -40,6 +42,7 @@ func Init(v *video.Video) *Menu { menu.icons = map[string]uint32{} menu.Push(buildTabs()) + menu.Push(buildHome()) menu.ContextReset() @@ -50,6 +53,16 @@ func Init(v *video.Video) *Menu { // OK on a menu entry. func (m *Menu) Push(s Scene) { m.stack = append(m.stack, s) + m.focus++ +} + +func haveTransparentBackground() bool { + for i := 0; i <= len(menu.stack)-1; i++ { + if menu.stack[i].Entry().label == "Quick Menu" { + return true + } + } + return false } // Render takes care of rendering the menu @@ -64,11 +77,11 @@ func (m *Menu) Render(dt float32) { w, h := m.GetFramebufferSize() m.ratio = float32(w) / 1920 - if state.CoreRunning { - m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor.Alpha(0.85)) - } else { - m.DrawRect(0, 0, float32(w), float32(h), 0, bgColor) + c := bgColor + if haveTransparentBackground() { + c = bgColor.Alpha(0.85) } + m.DrawImage(menu.icons["bg"], 0, 0, float32(w), float32(h), 1, 0, c) m.tweens.Update(dt) @@ -80,7 +93,9 @@ func (m *Menu) Render(dt float32) { m.stack[i].render() } - m.stack[currentScreenIndex].drawHintBar() + if m.focus-1 < len(m.stack) { + m.stack[m.focus-1].drawHintBar() + } } // ContextReset uploads the UI images to the GPU. @@ -102,10 +117,14 @@ func (m *Menu) ContextReset() { m.icons[filename] = video.NewImage(path) } - currentScreenIndex := len(m.stack) - 1 - curList := m.stack[currentScreenIndex].Entry() - for i := range curList.children { - curList.children[i].thumbnail = 0 + for h := 0; h < len(m.stack); h++ { + list := m.stack[h].Entry() + for i := range list.children { + list.children[i].thumbnail = 0 + for j := range list.children[i].children { + list.children[i].children[j].thumbnail = 0 + } + } } } @@ -116,8 +135,9 @@ func (m *Menu) WarpToQuickMenu() { m.stack = []Scene{} m.Push(buildTabs()) m.stack[0].segueNext() - m.Push(buildMainMenu()) + m.Push(buildHome()) m.stack[1].segueNext() m.Push(buildQuickMenu()) m.tweens.FastForward() + menu.focus = len(menu.stack) } diff --git a/menu/menu_test.go b/menu/menu_test.go index 4eec8f1f..8bc2b422 100644 --- a/menu/menu_test.go +++ b/menu/menu_test.go @@ -15,9 +15,9 @@ import ( func Test_WarpToQuickMenu(t *testing.T) { m := Init(&video.Video{}) - t.Run("Starts with a single scene if no game is running", func(t *testing.T) { + t.Run("Starts with 2 scenes if no game is running", func(t *testing.T) { got := len(menu.stack) - want := 1 + want := 2 if !reflect.DeepEqual(got, want) { t.Errorf("got = %v, want %v", got, want) } @@ -25,7 +25,7 @@ func Test_WarpToQuickMenu(t *testing.T) { t.Run("Starts on the tabs scene if no game is running", func(t *testing.T) { got := menu.stack[0].Entry().label - want := "Ludo" + want := "Tabs" if !reflect.DeepEqual(got, want) { t.Errorf("got = %v, want %v", got, want) } diff --git a/menu/notifications.go b/menu/notifications.go index b71945c7..c3072574 100644 --- a/menu/notifications.go +++ b/menu/notifications.go @@ -21,8 +21,6 @@ var severityBgColor = map[ntf.Severity]video.Color{ // RenderNotifications draws the list of notification messages on the viewport func (m *Menu) RenderNotifications() { - fbw, fbh := m.GetFramebufferSize() - m.Font.UpdateResolution(fbw, fbh) var h float32 = 75 stack := h for _, n := range ntf.List() { diff --git a/menu/palette.go b/menu/palette.go index c392ea3e..7a7c1e0f 100644 --- a/menu/palette.go +++ b/menu/palette.go @@ -9,14 +9,18 @@ import ( var white = video.Color{R: 1, G: 1, B: 1, A: 1} var black = video.Color{R: 0, G: 0, B: 0, A: 1} -var orange = video.Color{R: 0.8, G: 0.4, B: 0.1, A: 1} -var cyan = video.Color{R: 0.8784, G: 1, B: 1, A: 1} -var darkBlue = video.Color{R: 0.1, G: 0.1, B: 0.4, A: 1} +var blue = video.Color{R: 0.129, G: 0.441, B: 0.684, A: 1} +// var orange = video.Color{R: 0.8, G: 0.4, B: 0.1, A: 1} +// var cyan = video.Color{R: 0.8784, G: 1, B: 1, A: 1} +var darkBlue = video.Color{R: 0.1, G: 0.15, B: 0.4, A: 1} +var lightBlue = video.Color{R: 0.329, G: 0.641, B: 0.884, A: 1} var lightGrey = video.Color{R: 0.75, G: 0.75, B: 0.75, A: 1} var mediumGrey = video.Color{R: 0.5, G: 0.5, B: 0.5, A: 1} var darkGrey = video.Color{R: 0.25, G: 0.25, B: 0.25, A: 1} -var darkerGrey = video.Color{R: 0.10, G: 0.10, B: 0.10, A: 1} +// var darkerGrey = video.Color{R: 0.10, G: 0.10, B: 0.10, A: 1} +// var ultraDarkerGrey = video.Color{R: 0.05, G: 0.05, B: 0.05, A: 1} +var ultraDarkerBlue = video.Color{R: 0, G: 0.05, B: 0.15, A: 1} var darkInfo = video.Color{R: 0.04, G: 0.36, B: 0.46, A: 1} var lightInfo = video.Color{R: 0.53, G: 0.89, B: 1.00, A: 1} @@ -31,18 +35,37 @@ var darkWarning = video.Color{R: 0.47, G: 0.40, B: 0.04, A: 1} var lightWarning = video.Color{R: 1.00, G: 0.92, B: 0.53, A: 1} var bgColor = white -var cursorBg = cyan +var cursorBg = white var textColor = black +var sepColor = lightGrey +var hintTextColor = darkGrey +var hintBgColor = white +var tabTextColor = blue +var tabBgColor = white +var titleColor = darkBlue // UpdatePalette updates the color palette to honor the dark theme func (m *Menu) UpdatePalette() { bgColor = white - cursorBg = cyan + cursorBg = white textColor = black + sepColor = lightGrey + hintTextColor = darkGrey + hintBgColor = white + tabTextColor = blue + tabBgColor = white + titleColor = darkBlue if state.CoreRunning || settings.Current.VideoDarkMode { - bgColor = darkerGrey + bgColor = ultraDarkerBlue cursorBg = darkBlue textColor = white + sepColor = darkBlue + hintTextColor = lightBlue + hintBgColor = ultraDarkerBlue + tabTextColor = lightBlue + tabBgColor = darkBlue + titleColor = lightBlue } } + diff --git a/menu/scene.go b/menu/scene.go index f3020a6b..de4fe1bd 100644 --- a/menu/scene.go +++ b/menu/scene.go @@ -1,6 +1,8 @@ package menu import ( + "math" + "github.com/libretro/ludo/state" "github.com/tanema/gween" "github.com/tanema/gween/ease" @@ -9,8 +11,13 @@ import ( // entry is a menu entry. It can also represent a scene. // The menu data is a tree of entries. type entry struct { - yp, scale float32 - width, margin float32 + alpha float32 + scale float32 + scroll float32 + y float32 + entryHeight float32 + height float32 + margin float32 label, subLabel string path string // full path of the rom linked to the entry system string // name of the game system @@ -19,22 +26,19 @@ type entry struct { iconAlpha float32 tagAlpha float32 subLabelAlpha float32 + borderAlpha float32 callbackOK func() // callback executed when user presses OK callbackX func() // callback executed when user presses X value func() interface{} stringValue func() string - widget func(*entry) // widget draw callback used in settings - incr func(int) // increment callback used in settings - tags []string // flags extracted from game title - thumbnail uint32 // thumbnail texture id - gameName string // title of the game in db, used for thumbnails - cursor struct { - alpha float32 - yp float32 - } - children []entry // children entries - ptr int // index of the active child - indexes []struct { + widget func(*entry, *entry, int) // widget draw callback used in settings + incr func(int) // increment callback used in settings + tags []string // flags extracted from game title + thumbnail uint32 // thumbnail texture id + gameName string // title of the game in db, used for thumbnails + children []entry // children entries + ptr int // index of the active child + indexes []struct { Char byte Index int } @@ -55,26 +59,12 @@ type Scene interface { // genericSegueMount is the smooth transition of the menu entries first appearance func genericSegueMount(list *entry) { - for i := range list.children { - e := &list.children[i] - - if i == list.ptr { - e.yp = 0.5 + 0.3 - e.scale = 1.5 - } else if i < list.ptr { - e.yp = 0.4 + 0.3 + 0.08*float32(i-list.ptr) - e.scale = 0.5 - } else if i > list.ptr { - e.yp = 0.6 + 0.3 + 0.08*float32(i-list.ptr) - e.scale = 0.5 - } - e.labelAlpha = 0 - e.iconAlpha = 0 - e.tagAlpha = 0 - e.subLabelAlpha = 0 + if list.entryHeight == 0 { + list.entryHeight = 100 } - list.cursor.alpha = 0 - list.cursor.yp = 0.5 + 0.3 + + list.scroll = -float32(list.ptr) * list.entryHeight + list.y = 300 genericAnimate(list) } @@ -85,90 +75,90 @@ func genericAnimate(list *entry) { e := &list.children[i] // performance improvement - // if math.Abs(float64(i-list.ptr)) > 6 && i > 6 && i < len(list.children)-6 { - // continue - // } + if math.Abs(float64(i-list.ptr)) > 8 { + continue + } - var yp, tagAlpha, subLabelAlpha, scale float32 + var tagAlpha, subLabelAlpha float32 if i == list.ptr { - yp = 0.5 tagAlpha = 1 subLabelAlpha = 1 - scale = 1.5 } else if i < list.ptr { - yp = 0.4 + 0.08*float32(i-list.ptr) tagAlpha = 0 subLabelAlpha = 0 - scale = 0.5 } else if i > list.ptr { - yp = 0.6 + 0.08*float32(i-list.ptr) tagAlpha = 0 subLabelAlpha = 0 - scale = 0.5 } - menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine) menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 1, 0.15, ease.OutSine) menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine) menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, tagAlpha, 0.15, ease.OutSine) menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, subLabelAlpha, 0.15, ease.OutSine) - menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine) } - menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 1, 0.15, ease.OutSine) - menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5, 0.15, ease.OutSine) + + margin := 32 + containerHeight := float32(1080 - 88 - 270 - margin*2) + contentHeight := float32(len(list.children)) * list.entryHeight + + scroll := float32(0) + if list.ptr >= 3 { + scroll = -float32(list.ptr-3) * list.entryHeight + } + + if -scroll > contentHeight-containerHeight { + scroll = -(contentHeight - containerHeight) + } + + if contentHeight < containerHeight { + scroll = 0 + } + + menu.tweens[&list.scroll] = gween.New(list.scroll, scroll, 0.15, ease.OutSine) + menu.tweens[&list.y] = gween.New(list.y, 0, 0.15, ease.OutSine) + menu.tweens[&list.alpha] = gween.New(list.alpha, 1, 0.15, ease.OutSine) } // genericSegueNext is a smooth transition that fades out the current list // to leave room for the next list to appear func genericSegueNext(list *entry) { for i := range list.children { - e := &list.children[i] - - var yp, scale float32 - if i == list.ptr { - yp = 0.5 - 0.3 - scale = 1.5 - } else if i < list.ptr { - yp = 0.4 - 0.3 + 0.08*float32(i-list.ptr) - scale = 0.5 - } else if i > list.ptr { - yp = 0.6 - 0.3 + 0.08*float32(i-list.ptr) - scale = 0.5 + // performance improvement + if math.Abs(float64(i-list.ptr)) > 8 { + continue } - menu.tweens[&e.yp] = gween.New(e.yp, yp, 0.15, ease.OutSine) + e := &list.children[i] menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, 0, 0.15, ease.OutSine) menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine) menu.tweens[&e.tagAlpha] = gween.New(e.tagAlpha, 0, 0.15, ease.OutSine) menu.tweens[&e.subLabelAlpha] = gween.New(e.subLabelAlpha, 0, 0.15, ease.OutSine) - menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine) } - menu.tweens[&list.cursor.alpha] = gween.New(list.cursor.alpha, 0, 0.15, ease.OutSine) - menu.tweens[&list.cursor.yp] = gween.New(list.cursor.yp, 0.5-0.3, 0.15, ease.OutSine) + menu.tweens[&list.alpha] = gween.New(list.alpha, 0, 0.15, ease.OutSine) + menu.tweens[&list.y] = gween.New(list.y, -300, 0.15, ease.OutSine) } // genericDrawCursor draws the blinking rectangular background of the active // menu entry -func genericDrawCursor(list *entry) { - w, h := menu.GetFramebufferSize() - menu.DrawImage(menu.icons["arrow"], - 530*menu.ratio, float32(h)*list.cursor.yp-35*menu.ratio, - 70*menu.ratio, 70*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha)) - menu.DrawRect( - 550*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio, - float32(w)-630*menu.ratio, 100*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha)) -} - -// thumbnailDrawCursor draws the blinking rectangular background of the active -// menu entry when there is a thumbnail -func thumbnailDrawCursor(list *entry) { - w, h := menu.GetFramebufferSize() - menu.DrawImage(menu.icons["arrow"], - 500*menu.ratio, float32(h)*list.cursor.yp-50*menu.ratio, - 100*menu.ratio, 100*menu.ratio, 1, cursorBg.Alpha(list.cursor.alpha)) +func genericDrawCursor(list *entry, i int) { + w, _ := menu.Window.GetFramebufferSize() + y := list.y + (270+32)*menu.ratio + list.scroll*menu.ratio + list.entryHeight*float32(i)*menu.ratio + if menu.focus > 1 { + blink := float32(math.Cos(menu.t)) + menu.DrawImage( + menu.icons["selection"], + 360*menu.ratio-8*menu.ratio, + y-8*menu.ratio, + float32(w)-720*menu.ratio+16*menu.ratio, + list.entryHeight*menu.ratio+16*menu.ratio, + 1, 0.15, white.Alpha(list.alpha-list.alpha*blink)) + } menu.DrawRect( - 530*menu.ratio, float32(h)*list.cursor.yp-120*menu.ratio, - float32(w)-630*menu.ratio, 240*menu.ratio, 0.2, cursorBg.Alpha(list.cursor.alpha)) + 360*menu.ratio, + y, + float32(w)-720*menu.ratio, + list.entryHeight*menu.ratio, 0.1, + cursorBg.Alpha(list.alpha)) } // genericRender renders a vertical list of menu entries @@ -176,37 +166,70 @@ func thumbnailDrawCursor(list *entry) { func genericRender(list *entry) { w, h := menu.GetFramebufferSize() - genericDrawCursor(list) + menu.BoldFont.SetColor(titleColor.Alpha(list.alpha)) + menu.BoldFont.Printf( + 360*menu.ratio, + list.y*menu.ratio+230*menu.ratio, + 0.5*menu.ratio, list.label) + + menu.DrawRect( + 360*menu.ratio, + list.y*menu.ratio+270*menu.ratio, + float32(w)-720*menu.ratio, + 2*menu.ratio, + 0, sepColor.Alpha(list.alpha), + ) + + menu.ScissorStart( + int32(360*menu.ratio-8*menu.ratio), 0, + int32(float32(w)-720*menu.ratio+16*menu.ratio), int32(h)-int32(272*menu.ratio+list.y*menu.ratio)) - menu.ScissorStart(int32(530*menu.ratio), 0, int32(1310*menu.ratio), int32(h)) + fontOffset := 12 * menu.ratio - for _, e := range list.children { - if e.yp < -0.1 || e.yp > 1.1 { + for i, e := range list.children { + // performance improvement + if math.Abs(float64(i-list.ptr)) > 8 { continue } - fontOffset := 64 * 0.7 * menu.ratio * 0.3 + y := list.y*menu.ratio + + (270+32)*menu.ratio + + list.scroll*menu.ratio + + list.entryHeight*float32(i)*menu.ratio + + list.entryHeight/2*menu.ratio + + menu.DrawRect( + 360*menu.ratio, + y-1*menu.ratio+list.entryHeight/2*menu.ratio, + float32(w)-720*menu.ratio, + 2*menu.ratio, + 0, sepColor.Alpha(e.iconAlpha), + ) + + if i == list.ptr { + genericDrawCursor(list, i) + } menu.DrawImage(menu.icons[e.icon], - 610*menu.ratio-64*0.5*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*0.5*menu.ratio+fontOffset, + 420*menu.ratio-64*0.35*menu.ratio, + y-64*0.35*menu.ratio, 128*menu.ratio, 128*menu.ratio, - 0.5, textColor.Alpha(e.iconAlpha)) + 0.35, 0, textColor.Alpha(e.iconAlpha)) if e.labelAlpha > 0 { menu.Font.SetColor(textColor.Alpha(e.labelAlpha)) menu.Font.Printf( - 670*menu.ratio, - float32(h)*e.yp+fontOffset, + 480*menu.ratio, + y+fontOffset, 0.5*menu.ratio, e.label) if e.widget != nil { - e.widget(&e) + e.widget(list, &e, i) } else if e.stringValue != nil { lw := menu.Font.Width(0.5*menu.ratio, e.stringValue()) menu.Font.Printf( - float32(w)-lw-128*menu.ratio, - float32(h)*e.yp+fontOffset, + float32(w)-lw-400*menu.ratio, + y+fontOffset, 0.5*menu.ratio, e.stringValue()) } } @@ -221,58 +244,68 @@ func askQuitConfirmation(cb func()) { if !state.MenuActive { state.MenuActive = true } + menu.oldFocus = menu.focus menu.Push(buildYesNoDialog( "Confirm before quitting", "If you have not saved yet, your progress will be lost.", "Do you want to exit Ludo anyway?", func() { cb() })) + menu.focus = len(menu.stack) } else { cb() } } // Displays a confirmation dialog before deleting a playlist game entry -func askDeleteGameConfirmation(cb func()) { +/*func askDeleteGameConfirmation(cb func()) { + menu.oldFocus = menu.focus menu.Push(buildYesNoDialog( "Confirm before deleting", "You are about to delete a game entry.", "Games and game data won't be removed.", func() { cb() })) + menu.focus = len(menu.stack) } // Displays a confirmation dialog before deleting a playlist func askDeletePlaylistConfirmation(cb func()) { + menu.oldFocus = menu.focus menu.Push(buildYesNoDialog( "Confirm before deleting", "You are about to delete a playlist.", "Games and game data won't be removed.", func() { cb() })) -} + menu.focus = len(menu.stack) +}*/ // Displays a confirmation dialog before deleting a savestate func askDeleteSavestateConfirmation(cb func()) { + menu.oldFocus = menu.focus menu.Push(buildYesNoDialog( "Confirm before deleting", "You are about to delete a savestate.", "This action is irreversible.", func() { cb() })) + menu.focus = len(menu.stack) } func genericDrawHintBar() { - w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) + w, h := menu.Window.GetFramebufferSize() + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) _, upDown, _, a, b, _, _, _, _, guide := hintIcons() - var stack float32 + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, upDown, "Navigate", h) + stackHintRight(&rstack, a, "Ok", h) + stackHintRight(&rstack, b, "Back", h) if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) + stackHintRight(&rstack, guide, "Resume", h) } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) - stackHint(&stack, a, "OK", h) } diff --git a/menu/scene_core_disk_control.go b/menu/scene_core_disk_control.go index ef3dfd13..22883997 100644 --- a/menu/scene_core_disk_control.go +++ b/menu/scene_core_disk_control.go @@ -77,15 +77,22 @@ func (s *sceneCoreDiskControl) render() { func (s *sceneCoreDiskControl) drawHintBar() { w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) - - _, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons() - - var stack float32 + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) + + _, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons() + + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + list := menu.stack[len(menu.stack)-1].Entry() + stackHintLeft(&lstack, upDown, "Navigate", h) + if list.children[list.ptr].callbackOK != nil { + stackHintRight(&rstack, a, "Set", h) + } else { + stackHintLeft(&lstack, leftRight, "Set", h) + } + stackHintRight(&rstack, b, "Back", h) if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) + stackHintRight(&rstack, guide, "Resume", h) } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) - stackHint(&stack, leftRight, "SET", h) } diff --git a/menu/scene_core_options.go b/menu/scene_core_options.go index e347d7dd..97215112 100644 --- a/menu/scene_core_options.go +++ b/menu/scene_core_options.go @@ -81,15 +81,17 @@ func (s *sceneCoreOptions) render() { func (s *sceneCoreOptions) drawHintBar() { w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) _, upDown, leftRight, _, b, _, _, _, _, guide := hintIcons() - var stack float32 + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, upDown, "Navigate", h) + stackHintRight(&rstack, leftRight, "Set", h) + stackHintRight(&rstack, b, "Back", h) if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) + stackHintRight(&rstack, guide, "Resume", h) } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) - stackHint(&stack, leftRight, "SET", h) } diff --git a/menu/scene_dialog.go b/menu/scene_dialog.go index 7da8c35e..2ba9e011 100644 --- a/menu/scene_dialog.go +++ b/menu/scene_dialog.go @@ -49,6 +49,7 @@ func (s *sceneDialog) update(dt float32) { audio.PlayEffect(audio.Effects["cancel"]) menu.stack[len(menu.stack)-2].segueBack() menu.stack = menu.stack[:len(menu.stack)-1] + menu.focus = menu.oldFocus } } @@ -70,7 +71,7 @@ func (s *sceneDialog) render() { white, ) - menu.Font.SetColor(orange) + menu.Font.SetColor(titleColor) lw1 := menu.Font.Width(0.7*menu.ratio, s.title) menu.Font.Printf(fw/2-lw1/2, fh/2-120*menu.ratio+20*menu.ratio, 0.7*menu.ratio, s.title) menu.Font.SetColor(black) @@ -89,7 +90,7 @@ func (s *sceneDialog) render() { b, fw/2-width/2*menu.ratio+margin*menu.ratio, fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio, - 70*menu.ratio, 70*menu.ratio, 1.0, darkGrey) + 70*menu.ratio, 70*menu.ratio, 1.0, 0, darkGrey) menu.Font.Printf( fw/2-width/2*menu.ratio+margin*menu.ratio+70*menu.ratio, fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio, @@ -100,7 +101,7 @@ func (s *sceneDialog) render() { a, fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio, fh/2+height/2*menu.ratio-70*menu.ratio-margin*menu.ratio, - 70*menu.ratio, 70*menu.ratio, 1.0, darkGrey) + 70*menu.ratio, 70*menu.ratio, 1.0, 0, darkGrey) menu.Font.Printf( fw/2+width/2*menu.ratio-150*menu.ratio-margin*menu.ratio+70*menu.ratio, fh/2+height/2*menu.ratio-23*menu.ratio-margin*menu.ratio, diff --git a/menu/scene_explorer.go b/menu/scene_explorer.go index 34e43b4d..f849f809 100644 --- a/menu/scene_explorer.go +++ b/menu/scene_explorer.go @@ -110,7 +110,7 @@ func appendNode(list *sceneExplorer, fullPath string, name string, f os.FileInfo func buildExplorer(path string, exts []string, cb func(string), dirAction *entry, prettifier Prettifier) Scene { var list sceneExplorer - list.label = "Explorer" + list.label = path // Display the special directory action entry. if dirAction != nil && dirAction.label != "" { diff --git a/menu/scene_history.go b/menu/scene_history.go index c52b4ccd..63b3c587 100644 --- a/menu/scene_history.go +++ b/menu/scene_history.go @@ -10,41 +10,6 @@ import ( "github.com/libretro/ludo/state" ) -type sceneHistory struct { - entry -} - -func buildHistory() Scene { - var list sceneHistory - list.label = "History" - - history.Load() - for _, game := range history.List { - game := game // needed for callbackOK - strippedName, tags := extractTags(game.Name) - list.children = append(list.children, entry{ - label: strippedName, - subLabel: game.System, - gameName: game.Name, - path: game.Path, - system: game.System, - tags: tags, - callbackOK: func() { loadHistoryEntry(&list, game) }, - callbackX: func() { askDeleteGameConfirmation(func() { deleteHistoryEntry(&list, game) }) }, - }) - } - - if len(history.List) == 0 { - list.children = append(list.children, entry{ - label: "Empty history", - icon: "subsetting", - }) - } - - list.segueMount() - return &list -} - func loadHistoryEntry(list Scene, game history.Game) { if _, err := os.Stat(game.Path); os.IsNotExist(err) { ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.") @@ -74,179 +39,11 @@ func loadHistoryEntry(list Scene, game history.Game) { System: game.System, CorePath: corePath, }) - list.segueNext() - menu.Push(buildQuickMenu()) - menu.tweens.FastForward() // position the elements without animating + history.Load() + menu.WarpToQuickMenu() state.MenuActive = false } else { - list.segueNext() - menu.Push(buildQuickMenu()) - } -} - -func removeHistoryGame(s []history.Game, game history.Game) []history.Game { - l := []history.Game{} - for _, g := range s { - if g.Path != game.Path { - l = append(l, g) - } - } - return l -} - -func removeHistoryEntry(s []entry, game history.Game) []entry { - l := []entry{} - for _, g := range s { - if g.path != game.Path { - l = append(l, g) - } - } - - return l -} - -func deleteHistoryEntry(list *sceneHistory, game history.Game) { - history.List = removeHistoryGame(history.List, game) - history.Save() - refreshTabs() - list.children = removeHistoryEntry(list.children, game) - - if len(history.List) == 0 { - list.children = append(list.children, entry{ - label: "Empty history", - icon: "subsetting", - }) - } - - if list.ptr >= len(list.children) { - list.ptr = len(list.children) - 1 - } - - buildIndexes(&list.entry) - genericAnimate(&list.entry) -} - -// Generic stuff -func (s *sceneHistory) Entry() *entry { - return &s.entry -} - -func (s *sceneHistory) segueMount() { - genericSegueMount(&s.entry) -} - -func (s *sceneHistory) segueNext() { - genericSegueNext(&s.entry) -} - -func (s *sceneHistory) segueBack() { - genericAnimate(&s.entry) -} - -func (s *sceneHistory) update(dt float32) { - genericInput(&s.entry, dt) -} - -// Override rendering -func (s *sceneHistory) render() { - list := &s.entry - - _, h := menu.GetFramebufferSize() - - thumbnailDrawCursor(list) - - menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h)) - - for i, e := range list.children { - if e.yp < -0.1 || e.yp > 1.1 { - freeThumbnail(list, i) - continue - } - - fontOffset := 64 * 0.7 * menu.ratio * 0.3 - - if e.labelAlpha > 0 { - drawThumbnail( - list, i, - e.system, e.gameName, - 680*menu.ratio-85*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset, - 170*menu.ratio, 128*menu.ratio, - e.scale, white.Alpha(e.iconAlpha), - ) - menu.DrawBorder( - 680*menu.ratio-85*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset, - 170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale, - textColor.Alpha(e.iconAlpha)) - if e.path == state.GamePath && e.path != "" { - menu.DrawCircle( - 680*menu.ratio, - float32(h)*e.yp-14*menu.ratio+fontOffset, - 90*menu.ratio*e.scale, - black.Alpha(e.iconAlpha)) - menu.DrawImage(menu.icons["resume"], - 680*menu.ratio-25*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset, - 50*menu.ratio, 50*menu.ratio, - e.scale, white.Alpha(e.iconAlpha)) - } - - // Offset on Y to vertically center label + sublabel if there is a sublabel - slOffset := float32(0) - if e.subLabel != "" { - slOffset = 30 * menu.ratio * e.subLabelAlpha - } - - menu.Font.SetColor(textColor.Alpha(e.labelAlpha)) - stack := 840 * menu.ratio - menu.Font.Printf( - 840*menu.ratio, - float32(h)*e.yp+fontOffset-slOffset, - 0.5*menu.ratio, e.label) - stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label))) - stack += 10 - - for _, tag := range e.tags { - if _, ok := menu.icons[tag]; ok { - stack += 20 - menu.DrawImage( - menu.icons[tag], - stack, float32(h)*e.yp-22*menu.ratio-slOffset, - 60*menu.ratio, 44*menu.ratio, 1.0, white.Alpha(e.tagAlpha)) - menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio-slOffset, - 60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4)) - stack += 60 * menu.ratio - } - } - - menu.Font.SetColor(mediumGrey.Alpha(e.subLabelAlpha)) - menu.Font.Printf( - 840*menu.ratio, - float32(h)*e.yp+fontOffset+60*menu.ratio-slOffset, - 0.5*menu.ratio, e.subLabel) - } - } - - menu.ScissorEnd() -} - -func (s *sceneHistory) drawHintBar() { - w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) - - _, upDown, _, a, b, x, _, _, _, guide := hintIcons() - - var stack float32 - if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) - } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) - stackHint(&stack, a, "RUN", h) - - list := menu.stack[len(menu.stack)-1].Entry() - if list.children[list.ptr].callbackX != nil { - stackHint(&stack, x, "DELETE", h) + menu.WarpToQuickMenu() + state.MenuActive = false } } diff --git a/menu/scene_home.go b/menu/scene_home.go new file mode 100644 index 00000000..502535fb --- /dev/null +++ b/menu/scene_home.go @@ -0,0 +1,401 @@ +package menu + +import ( + "math" + "sort" + + "github.com/libretro/ludo/audio" + "github.com/libretro/ludo/history" + "github.com/libretro/ludo/input" + "github.com/libretro/ludo/libretro" + "github.com/libretro/ludo/playlists" + "github.com/libretro/ludo/state" + "github.com/libretro/ludo/utils" + + "github.com/tanema/gween" + "github.com/tanema/gween/ease" +) + +type sceneHome struct { + entry + yptr int + yscroll float32 + xscrolls []float32 + xptrs []int +} + +func buildHome() Scene { + var list sceneHome + list.label = "Home" + + cat := 0 + history.Load() + if len(history.List) > 0 { + list.children = append(list.children, entry{ + label: "Recently played", + }) + list.xscrolls = append(list.xscrolls, 0) + list.xptrs = append(list.xptrs, 0) + + for _, game := range history.List { + game := game + strippedName, tags := extractTags(game.Name) + list.children[cat].children = append(list.children[cat].children, entry{ + label: strippedName, + gameName: game.Name, + tags: tags, + subLabel: game.System, + system: game.System, + callbackOK: func() { + loadHistoryEntry(&list, game) + }, + }) + } + cat++ + } + + playlists.Load() + + // To store the keys in slice in sorted order + var keys []string + for k := range playlists.Playlists { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, path := range keys { + path := path + filename := utils.FileName(path) + + list.children = append(list.children, entry{ + label: filename, + }) + list.xscrolls = append(list.xscrolls, 0) + list.xptrs = append(list.xptrs, 0) + + for _, game := range playlists.Playlists[path] { + game := game + strippedName, tags := extractTags(game.Name) + list.children[cat].children = append(list.children[cat].children, entry{ + label: strippedName, + gameName: game.Name, + path: game.Path, + tags: tags, + icon: utils.FileName(path) + "-content", + subLabel: filename, + system: filename, + callbackOK: func() { loadPlaylistEntry(&list, filename, game) }, + }) + } + cat++ + } + + if len(list.children) == 0 { + menu.focus-- + } + + list.segueMount() + + return &list +} + +func (s *sceneHome) Entry() *entry { + return &s.entry +} + +func (s *sceneHome) segueMount() { + s.alpha = 0 + for j := range s.children { + s.xscrolls[j] = 0 + } + s.y = 300 + + for j := range s.children { + ve := &s.children[j] + ve.labelAlpha = 0 + ve.height = 504 + 136 + + for i := range ve.children { + e := &s.children[j].children[i] + + if i == s.xptrs[j] { + e.labelAlpha = 1 + e.iconAlpha = 1 + e.scale = 2.1 + e.borderAlpha = 0 + } else if i < s.xptrs[j] { + e.labelAlpha = 0 + e.iconAlpha = 0 + e.scale = 1 + e.borderAlpha = 0 + } else { + e.labelAlpha = 0 + e.iconAlpha = 1 + e.scale = 1 + e.borderAlpha = 0 + } + } + } + + s.animate() +} + +func (s *sceneHome) segueBack() { + s.animate() +} + +func (s *sceneHome) animate() { + for j := range s.children { + ve := &s.children[j] + + labelAlpha := float32(1) + if j < s.yptr { + labelAlpha = 0 + } + menu.tweens[&ve.labelAlpha] = gween.New(ve.labelAlpha, labelAlpha, 0.15, ease.OutSine) + menu.tweens[&ve.height] = gween.New(ve.height, 504+136, 0.15, ease.OutSine) + + for i := range ve.children { + e := &s.children[j].children[i] + + var labelAlpha, iconAlpha, scale, borderAlpha float32 + if i == s.xptrs[j] { + labelAlpha = 1 + iconAlpha = 1 + scale = 2.1 + borderAlpha = 1 + } else { + labelAlpha = 0 + iconAlpha = 1 + scale = 1 + borderAlpha = 0 + } + if j < s.yptr { + labelAlpha = 0 + iconAlpha = 0 + borderAlpha = 0 + } + + menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine) + menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, iconAlpha, 0.15, ease.OutSine) + menu.tweens[&e.borderAlpha] = gween.New(e.borderAlpha, borderAlpha, 0.15, ease.OutSine) + menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine) + } + } + + for j := range s.children { + menu.tweens[&s.xscrolls[j]] = gween.New(s.xscrolls[j], float32(s.xptrs[j]*(320+32)), 0.15, ease.OutSine) + } + + vst := float32(0) + for j := range s.children { + if j == s.yptr { + break + } + vst += 504 + 136 + } + + menu.tweens[&s.yscroll] = gween.New(s.yscroll, vst, 0.15, ease.OutSine) + menu.tweens[&s.alpha] = gween.New(s.alpha, 1, 0.15, ease.OutSine) + menu.tweens[&s.y] = gween.New(s.y, 0, 0.15, ease.OutSine) +} + +func (s *sceneHome) segueNext() { + menu.tweens[&s.alpha] = gween.New(s.alpha, 0, 0.15, ease.OutSine) + menu.tweens[&s.y] = gween.New(s.y, -300, 0.15, ease.OutSine) + + for j := range s.children { + ve := &s.children[j] + for i := range ve.children { + e := &s.children[j].children[i] + menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine) + } + } +} + +func (s *sceneHome) update(dt float32) { + // Empty state + if len(s.children) == 0 { + menu.focus-- + return + } + + // Right + repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() { + if s.xptrs[s.yptr] < len(s.children[s.yptr].children)-1 { + s.xptrs[s.yptr]++ + audio.PlayEffect(audio.Effects["down"]) + menu.t = 0 + s.animate() + } + }) + + // Left + repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() { + if s.xptrs[s.yptr] > 0 { + s.xptrs[s.yptr]-- + audio.PlayEffect(audio.Effects["up"]) + menu.t = 0 + s.animate() + } + }) + + // Down + repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() { + if s.yptr < len(s.children)-1 { + s.yptr++ + audio.PlayEffect(audio.Effects["down"]) + menu.t = 0 + s.animate() + } + }) + + // Up + repeatUp(dt, input.NewState[0][libretro.DeviceIDJoypadUp] == 1, func() { + if s.yptr > 0 { + s.yptr-- + audio.PlayEffect(audio.Effects["up"]) + menu.t = 0 + s.animate() + } else if s.yptr == 0 && len(menu.stack) > 1 { + audio.PlayEffect(audio.Effects["cancel"]) + menu.stack[len(menu.stack)-2].segueBack() + menu.focus-- + menu.t = 0 + } + }) + + // OK + if input.Released[0][libretro.DeviceIDJoypadA] == 1 { + if len(s.children) > 0 && s.children[s.yptr].children[s.xptrs[s.yptr]].callbackOK != nil { + audio.PlayEffect(audio.Effects["ok"]) + s.segueNext() + s.children[s.yptr].children[s.xptrs[s.yptr]].callbackOK() + } + } + + // Cancel + if input.Released[0][libretro.DeviceIDJoypadB] == 1 { + if len(menu.stack) > 1 { + audio.PlayEffect(audio.Effects["cancel"]) + menu.stack[len(menu.stack)-2].segueBack() + menu.focus-- + } + } +} + +func (s sceneHome) render() { + vst := float32(0) + for j, ve := range s.children { + ve := ve + + menu.BoldFont.SetColor(titleColor.Alpha(ve.labelAlpha * s.alpha)) + menu.BoldFont.Printf( + 96*menu.ratio, + s.y*menu.ratio+230*menu.ratio+vst*menu.ratio-s.yscroll*menu.ratio, + 0.5*menu.ratio, ve.label) + + y := s.y + 272 + vst - s.yscroll + + vst += ve.height + + // performance improvement + if math.Abs(float64(j-s.yptr)) > 1 { + continue + } + + stackWidth := float32(96) + for i, e := range ve.children { + x := -s.xscrolls[j] + stackWidth + + stackWidth += 320*e.scale + e.margin + 32 + + // performance improvement + if math.Abs(float64(i-s.xptrs[j])) > 4 { + freeThumbnail(&ve, i) + continue + } + + if menu.focus == 2 && j == s.yptr && i == s.xptrs[s.yptr] { + blink := float32(math.Cos(menu.t)) + menu.DrawImage( + menu.icons["selection"], + x*menu.ratio-8*menu.ratio, + y*menu.ratio-8*menu.ratio, + 320*e.scale*menu.ratio+16*menu.ratio, 240*e.scale*menu.ratio+16*menu.ratio, + 1, 0.1, white.Alpha((e.borderAlpha-blink)*s.alpha)) + } + + drawThumbnail( + &ve, i, + e.system, e.gameName, + x*menu.ratio, + y*menu.ratio, + 320*e.scale*menu.ratio, 240*e.scale*menu.ratio, + 1, white.Alpha(e.iconAlpha*s.alpha)) + + menu.DrawImage( + menu.icons["border"], + x*menu.ratio, + y*menu.ratio, + 320*e.scale*menu.ratio, 240*e.scale*menu.ratio, + 1, 0.07, white.Alpha(e.iconAlpha*s.alpha)) + + menu.BoldFont.SetColor(textColor.Alpha(e.labelAlpha * s.alpha)) + menu.BoldFont.Printf( + (x+672+32)*menu.ratio, + (y+360)*menu.ratio, + 0.7*menu.ratio, e.label) + + menu.BoldFont.SetColor(mediumGrey.Alpha(e.labelAlpha * s.alpha)) + menu.BoldFont.Printf( + (x+672+32)*menu.ratio, + (y+430)*menu.ratio, + 0.5*menu.ratio, e.subLabel) + + stack := (x + 672 + 32) * menu.ratio + for _, tag := range e.tags { + if _, ok := menu.icons[tag]; ok { + menu.DrawRect(stack-1*menu.ratio, (y+500-35)*menu.ratio-1*menu.ratio, + 48*menu.ratio+2*menu.ratio, 35*menu.ratio+2*menu.ratio, 0.22, + mediumGrey.Alpha(e.labelAlpha*s.alpha)) + menu.DrawImage( + menu.icons[tag], + stack, (y+500-35)*menu.ratio, + 48*menu.ratio, 35*menu.ratio, 1.0, 0.2, + white.Alpha(e.labelAlpha*s.alpha)) + stack += 48 * menu.ratio + stack += 24 * menu.ratio + } + } + } + } + + if len(s.children) == 0 { + w, h := menu.Window.GetFramebufferSize() + menu.BoldFont.SetColor(black) + msg := "Welcome to Ludo, please scan your collection." + msgw := menu.BoldFont.Width(0.5*menu.ratio, msg) + menu.BoldFont.Printf( + float32(w)/2-msgw/2, + float32(h)/2, + 0.5*menu.ratio, msg) + } +} + +func (s sceneHome) drawHintBar() { + w, h := menu.Window.GetFramebufferSize() + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) + + arrows, _, _, a, b, _, _, _, _, guide := hintIcons() + + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, arrows, "Navigate", h) + stackHintRight(&rstack, a, "Run", h) + stackHintRight(&rstack, b, "Back", h) + if state.CoreRunning { + stackHintRight(&rstack, guide, "Resume", h) + } +} diff --git a/menu/scene_keyboard.go b/menu/scene_keyboard.go index 96765b23..1152e53c 100644 --- a/menu/scene_keyboard.go +++ b/menu/scene_keyboard.go @@ -157,8 +157,7 @@ func (s *sceneKeyboard) render() { ttw := 10 * ksp // Background - menu.DrawRect(0, 0, float32(w), float32(h), 0, - white.Alpha(s.alpha)) + menu.DrawRect(0, 0, float32(w), float32(h), 0, bgColor.Alpha(s.alpha)) // Label menu.Font.SetColor(black) @@ -169,7 +168,7 @@ func (s *sceneKeyboard) render() { // Value menu.DrawRect(float32(w)/2-ttw/2, s.y+float32(h)*0.25-ksz/2, ttw, ksz, 0, - video.Color{R: 0.95, G: 0.95, B: 0.95, A: 1}) + lightGrey) menu.Font.Printf( float32(w)/2-ttw/2+ksz/4, s.y+float32(h)*0.25-ksz/2+ksz*0.62, @@ -205,15 +204,17 @@ func (s *sceneKeyboard) render() { func (s *sceneKeyboard) drawHintBar() { w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) arrows, _, _, a, b, x, y, start, _, _ := hintIcons() - var stack float32 - stackHint(&stack, arrows, "SELECT", h) - stackHint(&stack, b, "BACK", h) - stackHint(&stack, x, "SHIFT", h) - stackHint(&stack, y, "DELETE", h) - stackHint(&stack, a, "INSERT", h) - stackHint(&stack, start, "DONE", h) + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, arrows, "Select", h) + stackHintRight(&rstack, start, "Done", h) + stackHintRight(&rstack, a, "Insert", h) + stackHintRight(&rstack, y, "Delete", h) + stackHintRight(&rstack, x, "Shift", h) + stackHintRight(&rstack, b, "Back", h) } diff --git a/menu/scene_main.go b/menu/scene_main.go index 5ee1d557..7bd58f14 100644 --- a/menu/scene_main.go +++ b/menu/scene_main.go @@ -6,8 +6,10 @@ import ( "path/filepath" "github.com/libretro/ludo/core" + "github.com/libretro/ludo/dat" "github.com/libretro/ludo/history" ntf "github.com/libretro/ludo/notifications" + "github.com/libretro/ludo/scanner" "github.com/libretro/ludo/settings" "github.com/libretro/ludo/state" "github.com/libretro/ludo/utils" @@ -63,21 +65,10 @@ func prettifyCoreName(in string) string { func buildMainMenu() Scene { var list sceneMain - list.label = "Main Menu" + list.label = "Manual Menu" usr, _ := user.Current() - if state.CoreRunning { - list.children = append(list.children, entry{ - label: "Quick Menu", - icon: "subsetting", - callbackOK: func() { - list.segueNext() - menu.Push(buildQuickMenu()) - }, - }) - } - list.children = append(list.children, entry{ label: "Load Core", icon: "subsetting", @@ -112,43 +103,6 @@ func buildMainMenu() Scene { }, }) - if state.LudOS { - list.children = append(list.children, entry{ - label: "Updater", - icon: "subsetting", - callbackOK: func() { - list.segueNext() - menu.Push(buildUpdater()) - }, - }) - - list.children = append(list.children, entry{ - label: "Reboot", - icon: "subsetting", - callbackOK: func() { - askQuitConfirmation(func() { cleanReboot() }) - }, - }) - - list.children = append(list.children, entry{ - label: "Shutdown", - icon: "subsetting", - callbackOK: func() { - askQuitConfirmation(func() { cleanShutdown() }) - }, - }) - } else { - list.children = append(list.children, entry{ - label: "Quit", - icon: "subsetting", - callbackOK: func() { - askQuitConfirmation(func() { - menu.SetShouldClose(true) - }) - }, - }) - } - list.segueMount() return &list @@ -167,15 +121,23 @@ func coreExplorerCb(path string) { func gameExplorerCb(path string) { if err := core.LoadGame(path); err != nil { ntf.DisplayAndLog(ntf.Error, "Core", err.Error()) - return + } else { + scanner.ScanFile(path, func(game dat.Game) { + name := game.Name + if name == "" { + name = utils.FileName(path) + } + history.Push(history.Game{ + Path: path, + Name: name, + System: game.System, + CorePath: state.CorePath, + }) + history.Load() + menu.WarpToQuickMenu() + }) + state.MenuActive = false } - history.Push(history.Game{ - Path: path, - Name: utils.FileName(path), - CorePath: state.CorePath, - }) - menu.WarpToQuickMenu() - state.MenuActive = false } // Shutdown the operating system diff --git a/menu/scene_playlist.go b/menu/scene_playlist.go index 92569751..0e17bb27 100644 --- a/menu/scene_playlist.go +++ b/menu/scene_playlist.go @@ -12,13 +12,9 @@ import ( "github.com/libretro/ludo/playlists" "github.com/libretro/ludo/settings" "github.com/libretro/ludo/state" - "github.com/libretro/ludo/utils" ) -type scenePlaylist struct { - entry -} - +/* func buildPlaylist(path string) Scene { var list scenePlaylist list.label = utils.FileName(path) @@ -53,6 +49,7 @@ func buildPlaylist(path string) Scene { list.segueMount() return &list } +*/ // Index first letters of entries to allow quick jump to the next or previous // letter @@ -87,7 +84,7 @@ func extractTags(name string) (string, []string) { return name, tags } -func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game) { +func loadPlaylistEntry(list Scene, playlist string, game playlists.Game) { if _, err := os.Stat(game.Path); os.IsNotExist(err) { ntf.DisplayAndLog(ntf.Error, "Menu", "Game not found.") return @@ -118,167 +115,12 @@ func loadPlaylistEntry(list *scenePlaylist, playlist string, game playlists.Game System: playlist, CorePath: corePath, }) - list.segueNext() - menu.Push(buildQuickMenu()) - menu.tweens.FastForward() // position the elements without animating + history.Load() + menu.WarpToQuickMenu() state.MenuActive = false } else { list.segueNext() - menu.Push(buildQuickMenu()) - } -} - -func removePlaylistGame(s []playlists.Game, game playlists.Game) []playlists.Game { - l := []playlists.Game{} - for _, g := range s { - if g.Path != game.Path { - l = append(l, g) - } - } - return l -} - -func removePlaylistEntry(s []entry, game playlists.Game) []entry { - l := []entry{} - for _, g := range s { - if g.path != game.Path { - l = append(l, g) - } - } - - return l -} - -func deletePlaylistEntry(list *scenePlaylist, path string, game playlists.Game) { - playlists.Playlists[path] = removePlaylistGame(playlists.Playlists[path], game) - playlists.Save(path) - refreshTabs() - list.children = removePlaylistEntry(list.children, game) - - if len(playlists.Playlists[path]) == 0 { - list.children = append(list.children, entry{ - label: "Empty playlist", - icon: "subsetting", - }) - } - - if list.ptr >= len(list.children) { - list.ptr = len(list.children) - 1 - } - - buildIndexes(&list.entry) - genericAnimate(&list.entry) -} - -// Generic stuff -func (s *scenePlaylist) Entry() *entry { - return &s.entry -} - -func (s *scenePlaylist) segueMount() { - genericSegueMount(&s.entry) -} - -func (s *scenePlaylist) segueNext() { - genericSegueNext(&s.entry) -} - -func (s *scenePlaylist) segueBack() { - genericAnimate(&s.entry) -} - -func (s *scenePlaylist) update(dt float32) { - genericInput(&s.entry, dt) -} - -// Override rendering -func (s *scenePlaylist) render() { - list := &s.entry - - _, h := menu.GetFramebufferSize() - - thumbnailDrawCursor(list) - - menu.ScissorStart(int32(510*menu.ratio), 0, int32(1310*menu.ratio), int32(h)) - - for i, e := range list.children { - if e.yp < -0.1 || e.yp > 1.1 { - freeThumbnail(list, i) - continue - } - - fontOffset := 64 * 0.7 * menu.ratio * 0.3 - - if e.labelAlpha > 0 { - drawThumbnail( - list, i, - list.label, e.gameName, - 680*menu.ratio-85*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset, - 170*menu.ratio, 128*menu.ratio, - e.scale, white.Alpha(e.iconAlpha), - ) - menu.DrawBorder( - 680*menu.ratio-85*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset, - 170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale, - textColor.Alpha(e.iconAlpha)) - if e.path == state.GamePath && e.path != "" { - menu.DrawCircle( - 680*menu.ratio, - float32(h)*e.yp-14*menu.ratio+fontOffset, - 90*menu.ratio*e.scale, - black.Alpha(e.iconAlpha)) - menu.DrawImage(menu.icons["resume"], - 680*menu.ratio-25*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset, - 50*menu.ratio, 50*menu.ratio, - e.scale, white.Alpha(e.iconAlpha)) - } - - menu.Font.SetColor(textColor.Alpha(e.labelAlpha)) - stack := 840 * menu.ratio - menu.Font.Printf( - 840*menu.ratio, - float32(h)*e.yp+fontOffset, - 0.5*menu.ratio, e.label) - stack += float32(int(menu.Font.Width(0.5*menu.ratio, e.label))) - stack += 10 - - for _, tag := range e.tags { - if _, ok := menu.icons[tag]; ok { - stack += 20 - menu.DrawImage( - menu.icons[tag], - stack, float32(h)*e.yp-22*menu.ratio, - 60*menu.ratio, 44*menu.ratio, 1.0, white.Alpha(e.tagAlpha)) - menu.DrawBorder(stack, float32(h)*e.yp-22*menu.ratio, - 60*menu.ratio, 44*menu.ratio, 0.05/menu.ratio, black.Alpha(e.tagAlpha/4)) - stack += 60 * menu.ratio - } - } - } - } - - menu.ScissorEnd() -} - -func (s *scenePlaylist) drawHintBar() { - w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) - - _, upDown, _, a, b, x, _, _, _, guide := hintIcons() - - var stack float32 - if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) - } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) - stackHint(&stack, a, "RUN", h) - - list := menu.stack[len(menu.stack)-1].Entry() - if list.children[list.ptr].callbackX != nil { - stackHint(&stack, x, "DELETE", h) + menu.WarpToQuickMenu() + state.MenuActive = false } } diff --git a/menu/scene_quick.go b/menu/scene_quick.go index ac2e16c2..edfa7295 100644 --- a/menu/scene_quick.go +++ b/menu/scene_quick.go @@ -58,7 +58,7 @@ func buildQuickMenu() Scene { list.children = append(list.children, entry{ label: "Options", - icon: "subsetting", + icon: "options", callbackOK: func() { list.segueNext() menu.Push(buildCoreOptions()) @@ -76,6 +76,15 @@ func buildQuickMenu() Scene { }) } + list.children = append(list.children, entry{ + label: "Settings", + icon: "subsetting", + callbackOK: func() { + list.segueNext() + menu.Push(buildSettings()) + }, + }) + list.segueMount() return &list diff --git a/menu/scene_savestates.go b/menu/scene_savestates.go index 950888f6..9e02a4f2 100644 --- a/menu/scene_savestates.go +++ b/menu/scene_savestates.go @@ -1,6 +1,7 @@ package menu import ( + "math" "os" "path/filepath" "sort" @@ -20,6 +21,7 @@ type sceneSavestates struct { func buildSavestates() Scene { var list sceneSavestates list.label = "Savestates" + list.entryHeight = 160 list.children = append(list.children, entry{ label: "Save State", @@ -119,71 +121,105 @@ func deleteSavestateEntry(list *sceneSavestates, path string) { // Override rendering func (s *sceneSavestates) render() { list := &s.entry + w, h := menu.GetFramebufferSize() + + menu.BoldFont.SetColor(titleColor.Alpha(list.alpha)) + menu.BoldFont.Printf( + 360*menu.ratio, + list.y*menu.ratio+230*menu.ratio, + 0.5*menu.ratio, list.label) + + menu.DrawRect( + 360*menu.ratio, + list.y*menu.ratio+270*menu.ratio, + float32(w)-720*menu.ratio, + 2*menu.ratio, + 0, sepColor, + ) - _, h := menu.GetFramebufferSize() + menu.ScissorStart( + int32(360*menu.ratio-8*menu.ratio), 0, + int32(float32(w)-720*menu.ratio+16*menu.ratio), int32(h)-int32(272*menu.ratio+list.y*menu.ratio)) - thumbnailDrawCursor(list) + fontOffset := 12 * menu.ratio for i, e := range list.children { - if e.yp < -0.1 || e.yp > 1.1 { + // performance improvement + if math.Abs(float64(i-list.ptr)) > 8 { + freeThumbnail(list, i) continue } - fontOffset := 64 * 0.7 * menu.ratio * 0.3 + y := list.y*menu.ratio + + (270+32)*menu.ratio + + list.scroll*menu.ratio + + list.entryHeight*float32(i)*menu.ratio + + list.entryHeight/2*menu.ratio + + menu.DrawRect( + 360*menu.ratio, + y-1*menu.ratio+list.entryHeight/2*menu.ratio, + float32(w)-720*menu.ratio, + 2*menu.ratio, + 0, sepColor, + ) + + if i == list.ptr { + genericDrawCursor(list, i) + } if e.labelAlpha > 0 { drawSavestateThumbnail( list, i, filepath.Join(settings.Current.ScreenshotsDirectory, utils.FileName(e.path)+".png"), - 680*menu.ratio-85*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset, + 480*menu.ratio-85*1*menu.ratio, + y-64*menu.ratio, 170*menu.ratio, 128*menu.ratio, - e.scale, textColor.Alpha(e.iconAlpha), + 1, white.Alpha(e.iconAlpha), ) - menu.DrawBorder( - 680*menu.ratio-85*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-64*e.scale*menu.ratio+fontOffset, - 170*menu.ratio*e.scale, 128*menu.ratio*e.scale, 0.02/e.scale, - textColor.Alpha(e.iconAlpha)) if i == 0 { menu.DrawImage(menu.icons["savestate"], - 680*menu.ratio-25*e.scale*menu.ratio, - float32(h)*e.yp-14*menu.ratio-25*e.scale*menu.ratio+fontOffset, + 480*menu.ratio-25*1*menu.ratio, + y-50/2*menu.ratio, 50*menu.ratio, 50*menu.ratio, - e.scale, textColor.Alpha(e.iconAlpha)) + 1, 0, white.Alpha(e.iconAlpha)) } menu.Font.SetColor(textColor.Alpha(e.labelAlpha)) menu.Font.Printf( - 840*menu.ratio, - float32(h)*e.yp+fontOffset, + 600*menu.ratio, + y+fontOffset, 0.5*menu.ratio, e.label) } } + + menu.ScissorEnd() } func (s *sceneSavestates) drawHintBar() { w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) ptr := menu.stack[len(menu.stack)-1].Entry().ptr _, upDown, _, a, b, x, _, _, _, guide := hintIcons() - var stack float32 - if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) - } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, upDown, "Navigate", h) if ptr == 0 { - stackHint(&stack, a, "SAVE", h) + stackHintRight(&rstack, a, "Save", h) } else { - stackHint(&stack, a, "LOAD", h) + stackHintRight(&rstack, a, "Load", h) + } + stackHintRight(&rstack, b, "Back", h) + if state.CoreRunning { + stackHintRight(&rstack, guide, "Resume", h) } list := menu.stack[len(menu.stack)-1].Entry() if list.children[list.ptr].callbackX != nil { - stackHint(&stack, x, "DELETE", h) + stackHintRight(&rstack, x, "Delete", h) } } diff --git a/menu/scene_settings.go b/menu/scene_settings.go index c5e93bf1..1a74cb13 100644 --- a/menu/scene_settings.go +++ b/menu/scene_settings.go @@ -126,27 +126,27 @@ func dirExplorerCb(path string, f *structs.Field) { } // Widgets to display settings values -var widgets = map[string]func(*entry){ +var widgets = map[string]func(*entry, *entry, int){ // On/Off switch for boolean settings - "switch": func(e *entry) { + "switch": func(list, e *entry, i int) { icon := "off" if e.value().(bool) { icon = "on" } - w, h := menu.GetFramebufferSize() + fbw, _ := menu.GetFramebufferSize() menu.DrawImage(menu.icons[icon], - float32(w)-128*menu.ratio-128*menu.ratio, - float32(h)*e.yp-64*1.25*menu.ratio, + float32(fbw)-400*menu.ratio-128*menu.ratio, + list.y+(270+32)*menu.ratio+list.scroll*menu.ratio+100*float32(i)*menu.ratio+50*menu.ratio-64*1.25*menu.ratio, 128*menu.ratio, 128*menu.ratio, - 1.25, textColor.Alpha(e.iconAlpha)) + 1.25, 0, textColor.Alpha(e.iconAlpha)) }, // Range widget for audio volume and similat float settings - "range": func(e *entry) { - fbw, fbh := menu.GetFramebufferSize() - x := float32(fbw) - 128*menu.ratio - 175*menu.ratio - y := float32(fbh)*e.yp - 4*menu.ratio + "range": func(list, e *entry, i int) { + fbw, _ := menu.GetFramebufferSize() + x := float32(fbw) - 400*menu.ratio - 175*menu.ratio + y := list.y + (270+32)*menu.ratio + list.scroll*menu.ratio + 100*float32(i)*menu.ratio + 50*menu.ratio - 4*menu.ratio w := 175 * menu.ratio h := 8 * menu.ratio menu.DrawRect(x, y, w, h, 0.9, textColor.Alpha(e.iconAlpha/4)) @@ -274,20 +274,22 @@ func (s *sceneSettings) render() { func (s *sceneSettings) drawHintBar() { w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) _, upDown, leftRight, a, b, _, _, _, _, guide := hintIcons() - var stack float32 + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio list := menu.stack[len(menu.stack)-1].Entry() - if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) - } - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) + stackHintLeft(&lstack, upDown, "Navigate", h) if list.children[list.ptr].callbackOK != nil { - stackHint(&stack, a, "SET", h) + stackHintRight(&rstack, a, "Set", h) } else { - stackHint(&stack, leftRight, "SET", h) + stackHintLeft(&lstack, leftRight, "Set", h) + } + stackHintRight(&rstack, b, "Back", h) + if state.CoreRunning { + stackHintRight(&rstack, guide, "Resume", h) } } diff --git a/menu/scene_tabs.go b/menu/scene_tabs.go index b46401e1..1be3b9bc 100644 --- a/menu/scene_tabs.go +++ b/menu/scene_tabs.go @@ -1,25 +1,15 @@ package menu import ( - "fmt" - "os" - //"os/user" - "sort" + "math" + "time" "github.com/libretro/ludo/audio" "github.com/libretro/ludo/input" "github.com/libretro/ludo/libretro" - ntf "github.com/libretro/ludo/notifications" - "github.com/libretro/ludo/playlists" "github.com/libretro/ludo/scanner" - "github.com/libretro/ludo/state" - "github.com/libretro/ludo/utils" - "github.com/libretro/ludo/video" "github.com/libretro/ludo/settings" - colorful "github.com/lucasb-eyer/go-colorful" - - "github.com/tanema/gween" - "github.com/tanema/gween/ease" + "github.com/libretro/ludo/state" ) type sceneTabs struct { @@ -28,46 +18,42 @@ type sceneTabs struct { func buildTabs() Scene { var list sceneTabs - list.label = "Ludo" + list.label = "Tabs" list.children = append(list.children, entry{ - label: "Main Menu", - subLabel: "Load cores and games manually", - icon: "main", + icon: "tab-collection", callbackOK: func() { - menu.Push(buildMainMenu()) + menu.stack = menu.stack[:len(menu.stack)-1] + menu.Push(buildHome()) + menu.focus-- }, }) list.children = append(list.children, entry{ - label: "Settings", - subLabel: "Configure Ludo", - icon: "setting", + icon: "file", callbackOK: func() { - menu.Push(buildSettings()) + menu.stack = menu.stack[:len(menu.stack)-1] + menu.Push(buildMainMenu()) + menu.focus-- }, }) list.children = append(list.children, entry{ - label: "History", - subLabel: "Play again", - icon: "history", + icon: "tab-settings", callbackOK: func() { - menu.Push(buildHistory()) + menu.stack = menu.stack[:len(menu.stack)-1] + menu.Push(buildSettings()) + menu.focus-- }, }) - list.children = append(list.children, getPlaylists()...) - list.children = append(list.children, entry{ - label: "Add games", - subLabel: "Scan your collection", - icon: "add", + icon: "tab-scan", callbackOK: func() { //usr, _ := user.Current() menu.Push(buildExplorer(settings.Current.FileDirectory, nil, func(path string) { - scanner.ScanDir(path, refreshTabs) + scanner.ScanDir(path, nil) }, &entry{ label: "", @@ -75,257 +61,160 @@ func buildTabs() Scene { }, nil, )) + menu.focus-- }, }) - list.segueMount() - - return &list -} - -// refreshTabs is called after playlist scanning is complete. It inserts the new -// playlists in the tabs, and makes sure that all the icons are positioned and -// sized properly. -func refreshTabs() { - e := menu.stack[0].Entry() - l := len(e.children) - pls := getPlaylists() - - // This assumes that the 3 first tabs are not playlists, and that the last - // tab is the scanner. - e.children = append(e.children[:3], append(pls, e.children[l-1:]...)...) - - // Update which tab is the active tab after the refresh - if e.ptr >= 3 { - e.ptr += len(pls) - (l - 4) - } + if state.LudOS { + list.children = append(list.children, entry{ + icon: "tab-updater", + callbackOK: func() { + menu.stack = menu.stack[:len(menu.stack)-1] + menu.Push(buildUpdater()) + menu.focus-- + }, + }) - // Ensure new icons are styled properly - for i := range e.children { - if i == e.ptr { - e.children[i].iconAlpha = 1 - e.children[i].scale = 0.75 - e.children[i].width = 500 - } else if i < e.ptr { - e.children[i].iconAlpha = 1 - e.children[i].scale = 0.25 - e.children[i].width = 128 - } else if i > e.ptr { - e.children[i].iconAlpha = 1 - e.children[i].scale = 0.25 - e.children[i].width = 128 - } - } + list.children = append(list.children, entry{ + icon: "tab-reboot", + callbackOK: func() { + askQuitConfirmation(func() { cleanReboot() }) + }, + }) - // Adapt the tabs scroll value - if len(menu.stack) == 1 { - menu.scroll = float32(e.ptr * 128) + list.children = append(list.children, entry{ + icon: "tab-shutdown", + callbackOK: func() { + askQuitConfirmation(func() { cleanShutdown() }) + }, + }) } else { - e.children[e.ptr].margin = 1360 - menu.scroll = float32(e.ptr*128 + 680) - } -} - -// getPlaylists browse the filesystem for CSV files, parse them and returns -// a list of menu entries. It is used in the tabs, but could be used somewhere -// else too. -func getPlaylists() []entry { - playlists.Load() - - // To store the keys in slice in sorted order - var keys []string - for k := range playlists.Playlists { - keys = append(keys, k) - } - sort.Strings(keys) - - var pls []entry - for _, path := range keys { - path := path - filename := utils.FileName(path) - count := playlists.Count(path) - label := playlists.ShortName(filename) - pls = append(pls, entry{ - label: label, - subLabel: fmt.Sprintf("%d Games", count), - icon: filename, + list.children = append(list.children, entry{ + icon: "tab-quit", callbackOK: func() { - menu.Push(buildPlaylist(path)) + askQuitConfirmation(func() { + menu.SetShouldClose(true) + }) }, - callbackX: func() { askDeletePlaylistConfirmation(func() { deletePlaylist(path) }) }, }) } - return pls -} -func deletePlaylist(path string) { - err := os.Remove(path) - if err != nil { - ntf.DisplayAndLog(ntf.Error, "Menu", "Could not delete playlist: %s", err.Error()) - return - } - menu.stack[0].Entry().ptr++ - delete(playlists.Playlists, path) - refreshTabs() -} + list.segueMount() -func (tabs *sceneTabs) Entry() *entry { - return &tabs.entry + return &list } -func (tabs *sceneTabs) segueMount() { - for i := range tabs.children { - e := &tabs.children[i] - - if i == tabs.ptr { - e.labelAlpha = 1 - e.iconAlpha = 1 - e.scale = 0.75 - e.width = 500 - } else if i < tabs.ptr { - e.labelAlpha = 0 - e.iconAlpha = 1 - e.scale = 0.25 - e.width = 128 - } else if i > tabs.ptr { - e.labelAlpha = 0 - e.iconAlpha = 1 - e.scale = 0.25 - e.width = 128 - } - } - - tabs.animate() +func (s *sceneTabs) Entry() *entry { + return &s.entry } -func (tabs *sceneTabs) segueBack() { - tabs.animate() +func (s *sceneTabs) segueMount() { + s.animate() } -func (tabs *sceneTabs) animate() { - for i := range tabs.children { - e := &tabs.children[i] +func (s *sceneTabs) segueBack() { + s.animate() +} - var labelAlpha, scale, width float32 - if i == tabs.ptr { - labelAlpha = 1 - scale = 0.75 - width = 500 - } else if i < tabs.ptr { - labelAlpha = 0 - scale = 0.25 - width = 128 - } else if i > tabs.ptr { - labelAlpha = 0 - scale = 0.25 - width = 128 - } +func (s *sceneTabs) animate() { +} - menu.tweens[&e.labelAlpha] = gween.New(e.labelAlpha, labelAlpha, 0.15, ease.OutSine) - menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 1, 0.15, ease.OutSine) - menu.tweens[&e.scale] = gween.New(e.scale, scale, 0.15, ease.OutSine) - menu.tweens[&e.width] = gween.New(e.width, width, 0.15, ease.OutSine) - menu.tweens[&e.margin] = gween.New(e.margin, 0, 0.15, ease.OutSine) - } - menu.tweens[&menu.scroll] = gween.New(menu.scroll, float32(tabs.ptr*128), 0.15, ease.OutSine) +// left tabs are never removed, we don't need to implement this callback +func (s *sceneTabs) segueNext() { } -func (tabs *sceneTabs) segueNext() { - cur := &tabs.children[tabs.ptr] - menu.tweens[&cur.margin] = gween.New(cur.margin, 1360, 0.15, ease.OutSine) - menu.tweens[&menu.scroll] = gween.New(menu.scroll, menu.scroll+680, 0.15, ease.OutSine) - for i := range tabs.children { - e := &tabs.children[i] - if i != tabs.ptr { - menu.tweens[&e.iconAlpha] = gween.New(e.iconAlpha, 0, 0.15, ease.OutSine) +func (s *sceneTabs) update(dt float32) { + // Left + repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() { + s.ptr-- + if s.ptr < 0 { + s.ptr = 0 + } else { + audio.PlayEffect(audio.Effects["up"]) + menu.t = 0 + s.animate() } - } -} + }) -func (tabs *sceneTabs) update(dt float32) { // Right repeatRight(dt, input.NewState[0][libretro.DeviceIDJoypadRight] == 1, func() { - tabs.ptr++ - if tabs.ptr >= len(tabs.children) { - tabs.ptr = 0 + s.ptr++ + if s.ptr >= len(s.children) { + s.ptr = len(s.children) - 1 + } else { + audio.PlayEffect(audio.Effects["down"]) + menu.t = 0 + s.animate() } - audio.PlayEffect(audio.Effects["down"]) - tabs.animate() }) - // Left - repeatLeft(dt, input.NewState[0][libretro.DeviceIDJoypadLeft] == 1, func() { - tabs.ptr-- - if tabs.ptr < 0 { - tabs.ptr = len(tabs.children) - 1 - } - audio.PlayEffect(audio.Effects["up"]) - tabs.animate() + // Down + repeatDown(dt, input.NewState[0][libretro.DeviceIDJoypadDown] == 1, func() { + audio.PlayEffect(audio.Effects["ok"]) + menu.t = 0 + menu.focus++ + s.animate() }) // OK if input.Released[0][libretro.DeviceIDJoypadA] == 1 { - if tabs.children[tabs.ptr].callbackOK != nil { - audio.PlayEffect(audio.Effects["ok"]) - tabs.segueNext() - tabs.children[tabs.ptr].callbackOK() - } - } - - // X - if input.Released[0][libretro.DeviceIDJoypadX] == 1 { - if tabs.children[tabs.ptr].callbackX != nil { - tabs.children[tabs.ptr].callbackX() - } + audio.PlayEffect(audio.Effects["ok"]) + s.children[s.ptr].callbackOK() + menu.t = 0 + menu.focus++ + s.animate() } } -func (tabs sceneTabs) render() { - _, h := menu.GetFramebufferSize() +func (s sceneTabs) render() { + w, _ := menu.Window.GetFramebufferSize() - stackWidth := 710 * menu.ratio - for i, e := range tabs.children { + now := time.Now().Format("3:04PM") + nowWidth := menu.BoldFont.Width(0.5*menu.ratio, now) + menu.BoldFont.SetColor(textColor) + menu.BoldFont.Printf( + float32(w)-96*menu.ratio-nowWidth, + 90*menu.ratio, 0.5*menu.ratio, now) - cf := colorful.Hcl(float64(i)*20, 0.5, 0.5) - c := video.Color{R: float32(cf.R), G: float32(cf.B), B: float32(cf.G), A: e.iconAlpha} - - x := -menu.scroll*menu.ratio + stackWidth + e.width/2*menu.ratio - - stackWidth += e.width*menu.ratio + e.margin*menu.ratio + if menu.focus > 2 { + return + } - if e.labelAlpha > 0 { - menu.Font.SetColor(c.Alpha(e.labelAlpha)) - lw := menu.Font.Width(0.5*menu.ratio, e.label) - menu.Font.Printf(x-lw/2, float32(int(float32(h)/2+250*menu.ratio)), 0.5*menu.ratio, e.label) - lw = menu.Font.Width(0.4*menu.ratio, e.subLabel) - menu.Font.Printf(x-lw/2, float32(int(float32(h)/2+330*menu.ratio)), 0.4*menu.ratio, e.subLabel) + spacing := float32(96 + 32) + totalWidth := spacing * float32(len(s.children)) * menu.ratio + + for i, e := range s.children { + if i == s.ptr && menu.focus == 1 { + blink := float32(math.Cos(menu.t)) + menu.DrawImage(menu.icons["selection"], + float32(w)/2-totalWidth/2+float32(i)*spacing*menu.ratio+96*menu.ratio/2-8*menu.ratio, + 32*menu.ratio-8*menu.ratio, + 96*menu.ratio+16*menu.ratio, 96*menu.ratio+16*menu.ratio, 1, 1, + white.Alpha(1-blink)) } - - menu.DrawImage(menu.icons["hexagon"], - x-220*e.scale*menu.ratio, float32(h)/2-220*e.scale*menu.ratio, - 440*menu.ratio, 440*menu.ratio, e.scale, c) - + menu.DrawImage(menu.icons["circle"], + float32(w)/2-totalWidth/2+float32(i)*spacing*menu.ratio+96*menu.ratio/2, + 32*menu.ratio, + 96*menu.ratio, 96*menu.ratio, 1, 0, tabBgColor) menu.DrawImage(menu.icons[e.icon], - x-128*e.scale*menu.ratio, float32(h)/2-128*e.scale*menu.ratio, - 256*menu.ratio, 256*menu.ratio, e.scale, white.Alpha(e.iconAlpha)) + float32(w)/2-totalWidth/2+float32(i)*spacing*menu.ratio+96*menu.ratio/2+24*menu.ratio, + 56*menu.ratio, + 48*menu.ratio, 48*menu.ratio, 1, 0, tabTextColor) } } -func (tabs sceneTabs) drawHintBar() { - w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) +func (s *sceneTabs) drawHintBar() { + w, h := menu.Window.GetFramebufferSize() + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) - _, _, leftRight, a, _, x, _, _, _, guide := hintIcons() + _, _, leftRight, a, _, _, _, _, _, guide := hintIcons() - var stack float32 + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, leftRight, "Navigate", h) + stackHintRight(&rstack, a, "Ok", h) if state.CoreRunning { - stackHint(&stack, guide, "RESUME", h) - } - stackHint(&stack, leftRight, "NAVIGATE", h) - stackHint(&stack, a, "OPEN", h) - - list := menu.stack[0].Entry() - if list.children[list.ptr].callbackX != nil { - stackHint(&stack, x, "DELETE", h) + stackHintRight(&rstack, guide, "Resume", h) } } diff --git a/menu/scene_updater.go b/menu/scene_updater.go index 75845257..3c1e2ad1 100644 --- a/menu/scene_updater.go +++ b/menu/scene_updater.go @@ -16,11 +16,11 @@ type sceneUpdater struct { func buildUpdater() Scene { var list sceneUpdater - list.label = "Updater Menu" + list.label = "Update LudOS" list.children = append(list.children, entry{ label: "Checking updates", - icon: "reload", + icon: "reset", }) list.segueMount() @@ -83,11 +83,11 @@ func (s *sceneUpdater) update(dt float32) { if ludos.IsDownloading() { s.children[0].label = fmt.Sprintf( "Downloading update %.0f%%%%", ludos.GetProgress()*100) - s.children[0].icon = "reload" + s.children[0].icon = "reset" s.children[0].callbackOK = nil } else if ludos.IsDone() { - s.children[0].label = "Reboot and upgrade" - s.children[0].icon = "reload" + s.children[0].label = "Reboot and update" + s.children[0].icon = "reset" s.children[0].callbackOK = func() { cmd := exec.Command("/usr/sbin/shutdown", "-r", "now") core.UnloadGame() diff --git a/menu/scene_wifi.go b/menu/scene_wifi.go index 0af9d257..7e9b6765 100644 --- a/menu/scene_wifi.go +++ b/menu/scene_wifi.go @@ -15,7 +15,7 @@ func buildWiFi() Scene { list.children = append(list.children, entry{ label: "Looking for networks", - icon: "reload", + icon: "reset", }) list.segueMount() @@ -32,7 +32,7 @@ func buildWiFi() Scene { network := network list.children = append(list.children, entry{ label: network.SSID, - icon: "menu_network", + icon: "wifi", stringValue: func() string { return ludos.NetworkStatus(network) }, callbackOK: func() { list.segueNext() @@ -85,15 +85,17 @@ func (s *sceneWiFi) render() { } func (s *sceneWiFi) drawHintBar() { - w, h := menu.GetFramebufferSize() - menu.DrawRect(0, float32(h)-70*menu.ratio, float32(w), 70*menu.ratio, 0, lightGrey) + w, h := menu.Window.GetFramebufferSize() + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 88*menu.ratio, 0, hintBgColor) + menu.DrawRect(0, float32(h)-88*menu.ratio, float32(w), 2*menu.ratio, 0, sepColor) _, upDown, _, a, b, _, _, _, _, _ := hintIcons() - var stack float32 - stackHint(&stack, upDown, "NAVIGATE", h) - stackHint(&stack, b, "BACK", h) + lstack := float32(75) * menu.ratio + rstack := float32(w) - 96*menu.ratio + stackHintLeft(&lstack, upDown, "Navigate", h) if s.children[0].callbackOK != nil { - stackHint(&stack, a, "CONNECT", h) + stackHintRight(&rstack, a, "Connect", h) } + stackHintRight(&rstack, b, "Back", h) } diff --git a/menu/thumbnail.go b/menu/thumbnail.go index 280fda3e..c6e2c726 100644 --- a/menu/thumbnail.go +++ b/menu/thumbnail.go @@ -14,6 +14,9 @@ import ( // Downloads a thumbnail from the web and cache it to the local filesystem. func downloadThumbnail(list *entry, i int, url, folderPath, path string) { + if i >= len(list.children) { + return + } resp, err := http.Get(url) if err != nil { list.children[i].thumbnail = menu.icons["img-broken"] @@ -61,7 +64,7 @@ func scrubIllegalChars(str string) string { } // Draws a thumbnail in the playlist scene. -func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32, color video.Color) { +func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scale float32, c video.Color) { folderPath := filepath.Join(settings.Current.ThumbnailsDirectory, system, "Named_Snaps") legalName := scrubIllegalChars(gameName) path := filepath.Join(folderPath, legalName+".png") @@ -76,26 +79,18 @@ func drawThumbnail(list *entry, i int, system, gameName string, x, y, w, h, scal } } - menu.DrawImage( - list.children[i].thumbnail, - x, y, w, h, scale, - color, - ) + menu.DrawThumbnail(list.children[i].thumbnail, x, y, w, h, scale, 0.07, c) } // Draws a thumbnail in the savestates scene. -func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale float32, color video.Color) { +func drawSavestateThumbnail(list *entry, i int, path string, x, y, w, h, scale float32, c video.Color) { if list.children[i].thumbnail == 0 { if _, err := os.Stat(path); !os.IsNotExist(err) { list.children[i].thumbnail = video.NewImage(path) } } - menu.DrawImage( - list.children[i].thumbnail, - x, y, w, h, scale, - color, - ) + menu.DrawThumbnail(list.children[i].thumbnail, x, y, w, h, scale, 0, c) } func freeThumbnail(list *entry, i int) { diff --git a/scanner/scanner.go b/scanner/scanner.go index 65966ced..565172e2 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -70,11 +70,27 @@ func ScanDir(dir string, doneCb func()) { f.Close() i++ } - doneCb() + if doneCb != nil { + doneCb() + } n.Update(ntf.Success, "Done scanning. %d new games found.", i) }() } +// ScanFile scans a single file +func ScanFile(path string, doneCb func(dat.Game)) { + dir := filepath.Dir(path) + roms := []string{path} + games := make(chan (dat.Game)) + go Scan(dir, roms, games, nil) + go func() { + game := <-games + if doneCb != nil { + doneCb(game) + } + }() +} + // Returns the checksum and headerless checksum of a ROM func checksumHeaderless(rom *zip.File, headerSize uint) (uint32, uint32, error) { h, err := rom.Open() @@ -109,7 +125,9 @@ func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) // Open the ZIP archive z, err := zip.OpenReader(f) if err != nil { - n.Update(ntf.Error, err.Error()) + if n != nil { + n.Update(ntf.Error, err.Error()) + } continue } for _, rom := range z.File { @@ -128,14 +146,18 @@ func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) } else if rom.CRC32 > 0 { // Look for a matching game entry in the database state.DB.FindByCRC(f, rom.Name, rom.CRC32, size, games) - n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f) + if n != nil { + n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f) + } } } z.Close() case ".cue", ".pbp", ".m3u": // Look for a matching game entry in the database state.DB.FindByROMName(f, filepath.Base(f), 0, games) - n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f) + if n != nil { + n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f) + } case ".32x", ".a26", "a52", ".a78", ".col", ".crt", ".d64", ".pce", ".fds", ".gb", ".gba", ".gbc", ".gen", ".gg", ".ipf", ".j64", ".jag", ".lnx", ".md", ".n64", ".nes", ".ngc", ".nds", ".rom", ".sfc", ".sg", ".smc", ".smd", ".sms", ".ws", ".wsc", ".z64": bytes, err := os.ReadFile(f) if err != nil { @@ -153,7 +175,9 @@ func Scan(dir string, roms []string, games chan (dat.Game), n *ntf.Notification) crcHeaderless := crc32.ChecksumIEEE(bytes[headerSize:]) state.DB.FindByCRC(f, utils.FileName(f), crcHeaderless, s.Size()-int64(headerSize), games) } - n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f) + if n != nil { + n.Update(ntf.Info, strconv.Itoa(i)+"/"+strconv.Itoa(len(roms))+" "+f) + } } } close(games) diff --git a/video/demul_frag_shader.go b/video/demul_frag_shader.go index 23c48eec..da593842 100644 --- a/video/demul_frag_shader.go +++ b/video/demul_frag_shader.go @@ -20,7 +20,7 @@ uniform vec4 color; COMPAT_VARYING vec2 fragTexCoord; vec4 demultiply(vec4 c) { - return vec4(c.rgb/c.a, c.a); + return vec4(c.rgb/c.a, max(0.0, c.a)); } void main() { diff --git a/video/font.go b/video/font.go index ae682703..5f2a6c2f 100644 --- a/video/font.go +++ b/video/font.go @@ -230,8 +230,8 @@ func LoadFont(file string, scale int32, windowWidth int, windowHeight int) (*Fon } // SetColor allows you to set the text color to be used when you draw the text -func (f *Font) SetColor(color Color) { - f.color = color +func (f *Font) SetColor(c Color) { + f.color = c } // UpdateResolution passes the new framebuffer size to the font shader diff --git a/video/gfx.go b/video/gfx.go index ec614315..2b6f66e2 100644 --- a/video/gfx.go +++ b/video/gfx.go @@ -66,17 +66,44 @@ func rotateUV(va []float32, rot uint) []float32 { } // DrawImage draws an image with x, y, w, h -func (video *Video) DrawImage(image uint32, x, y, w, h float32, scale float32, c Color) { +func (video *Video) DrawImage(image uint32, x, y, w, h, scale, r float32, c Color) { va := video.vertexArray(x, y, w, h, scale) - gl.UseProgram(video.demulProgram) - gl.Uniform4f(gl.GetUniformLocation(video.demulProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A) + gl.UseProgram(video.roundedProgram) + gl.Uniform4f(gl.GetUniformLocation(video.roundedProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A) + gl.Uniform1f(gl.GetUniformLocation(video.roundedProgram, gl.Str("radius\x00")), r) + gl.Uniform2f(gl.GetUniformLocation(video.roundedProgram, gl.Str("size\x00")), w, h) + gl.Enable(gl.BLEND) + gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + bindVertexArray(video.vao) + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, image) + gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo) + gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW) + gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) + bindVertexArray(0) + gl.BindTexture(gl.TEXTURE_2D, 0) + gl.UseProgram(0) + gl.Disable(gl.BLEND) +} + +// DrawThumbnail draws an image with x, y, w, h +func (video *Video) DrawThumbnail(image uint32, x, y, w, h, scale, r float32, c Color) { + + va := video.vertexArray(x, y, w, h, scale) + + gl.UseProgram(video.roundedProgram) + gl.Uniform4f(gl.GetUniformLocation(video.roundedProgram, gl.Str("color\x00")), c.R, c.G, c.B, c.A) + gl.Uniform1f(gl.GetUniformLocation(video.roundedProgram, gl.Str("radius\x00")), r) + gl.Uniform2f(gl.GetUniformLocation(video.roundedProgram, gl.Str("size\x00")), w, h) gl.Enable(gl.BLEND) gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) bindVertexArray(video.vao) gl.ActiveTexture(gl.TEXTURE0) gl.BindTexture(gl.TEXTURE_2D, image) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) + gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo) gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW) gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) @@ -148,10 +175,13 @@ func (video *Video) DrawRect(x, y, w, h, r float32, c Color) { gl.Enable(gl.BLEND) gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) bindVertexArray(video.vao) + gl.ActiveTexture(gl.TEXTURE0) + gl.BindTexture(gl.TEXTURE_2D, video.white) gl.BindBuffer(gl.ARRAY_BUFFER, video.vbo) gl.BufferData(gl.ARRAY_BUFFER, len(va)*4, gl.Ptr(va), gl.STATIC_DRAW) gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) bindVertexArray(0) + gl.BindTexture(gl.TEXTURE_2D, 0) gl.UseProgram(0) gl.Disable(gl.BLEND) } @@ -219,3 +249,9 @@ func NewImage(file string) uint32 { return textureLoad(rgba) } + +func newWhite() uint32 { + rgba := image.NewRGBA(image.Rect(0, 0, 8, 8)) + draw.Draw(rgba, rgba.Bounds(), image.White, image.Point{0, 0}, draw.Src) + return textureLoad(rgba) +} diff --git a/video/rounded_frag_shader.go b/video/rounded_frag_shader.go index d9717cd5..d5455282 100644 --- a/video/rounded_frag_shader.go +++ b/video/rounded_frag_shader.go @@ -14,6 +14,7 @@ out vec4 COMPAT_FRAGCOLOR; #define COMPAT_FRAGCOLOR gl_FragColor #endif +uniform sampler2D Texture; uniform vec4 color; uniform float radius; uniform vec2 size; @@ -24,11 +25,15 @@ float udRoundBox(vec2 p, vec2 b, float r) { return length(max(abs(p)-b+r,0.0))-r; } +vec4 demultiply(vec4 c) { + return vec4(c.rgb/c.a, c.a); +} + void main() { float ratio = size.x / size.y; vec2 halfRes = vec2(0.5*ratio, 0.5); float b = udRoundBox(fragTexCoord*vec2(ratio,1.0) - halfRes, halfRes, min(halfRes.x,halfRes.y)*radius); vec4 c = min(color, vec4(1.0, 1.0, 1.0, 1.0)); - COMPAT_FRAGCOLOR = vec4(c.r, c.g, c.b, c.a * (1.0-smoothstep(0.00002,0.0001,b))); + COMPAT_FRAGCOLOR = demultiply(COMPAT_TEXTURE(Texture, fragTexCoord)) * vec4(c.r, c.g, c.b, c.a * (1.0-smoothstep(0.0001,0.001,b))); } ` + "\x00" diff --git a/video/video.go b/video/video.go index 8f978b41..8a2903b3 100644 --- a/video/video.go +++ b/video/video.go @@ -17,10 +17,12 @@ import ( // Video holds the state of the video package type Video struct { - Window *glfw.Window - Geom libretro.GameGeometry - Font *Font + Window *glfw.Window + Geom libretro.GameGeometry + Font *Font + BoldFont *Font + white uint32 // white texture for sampler2D program uint32 // current program used for the game quad defaultProgram uint32 // default program used for the game quad sharpBilinearProgram uint32 // sharp bilinear program used for the game quad @@ -126,6 +128,11 @@ func (video *Video) Configure(fullscreen bool) { if err != nil { panic(err) } + boldFontPath := filepath.Join(settings.Current.AssetsDirectory, "boldfont.ttf") + video.BoldFont, err = LoadFont(boldFontPath, int32(36*2), fbw, fbh) + if err != nil { + panic(err) + } // Configure the vertex and fragment shaders video.defaultProgram, err = newProgram(vertexShader, defaultFragmentShader) @@ -209,6 +216,8 @@ func (video *Video) Configure(fullscreen bool) { video.coreRatioViewport(fbw, fbh) + video.white = newWhite() + if e := gl.GetError(); e != gl.NO_ERROR { log.Printf("[Video] OpenGL error: %d\n", e) }