From c6b3499897fd81090f2e7f564dc8d4bf7fe84b0d Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 8 Jan 2024 21:26:24 -0500
Subject: [PATCH] Finished up hold note placement on gamepad, implemented note
 preview playhead

---
 source/Main.hx                                |   2 +
 .../ui/debug/charting/ChartEditorState.hx     | 130 ++++----
 .../handlers/ChartEditorGamepadHandler.hx     | 278 +++++++++++-------
 .../ui/haxeui/FlxGamepadActionInputSource.hx  |  53 ++++
 4 files changed, 301 insertions(+), 162 deletions(-)
 create mode 100644 source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx

diff --git a/source/Main.hx b/source/Main.hx
index 86e520e69..a7482c8d6 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -111,6 +111,8 @@ class Main extends Sprite
     Toolkit.init();
     Toolkit.theme = 'dark'; // don't be cringe
     Toolkit.autoScale = false;
+    // Don't focus on UI elements when they first appear.
+    haxe.ui.focus.FocusManager.instance.autoFocus = false;
     funkin.input.Cursor.registerHaxeUICursors();
     haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
   }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index f3236578a..1b9176174 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -366,8 +366,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       }
     }
 
-    updatePlayheadGhostHoldNotes();
-
     // Move the rendered notes to the correct position.
     renderedNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
     renderedHoldNotes.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
@@ -375,8 +373,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     renderedSelectionSquares.setPosition(gridTiledSprite?.x ?? 0.0, gridTiledSprite?.y ?? 0.0);
     // Offset the selection box start position, if we are dragging.
     if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
-    // Update the note preview viewport box.
+
+    // Update the note preview.
     setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
+    refreshNotePreviewPlayheadPosition();
+
     // Update the measure tick display.
     if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
     return this.scrollPositionInPixels;
@@ -438,6 +439,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     gridPlayhead.y = this.playheadPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
 
     updatePlayheadGhostHoldNotes();
+    refreshNotePreviewPlayheadPosition();
 
     return this.playheadPositionInPixels;
   }
@@ -1842,6 +1844,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var notePreviewViewport:Null<FlxSliceSprite> = null;
 
+  /**
+   * The thin sprite used for representing the playhead on the note preview.
+   * We move this up and down to represent the current position.
+   */
+  var notePreviewPlayhead:Null<FlxSprite> = null;
+
   /**
    * The rectangular sprite used for rendering the selection box.
    * Uses a 9-slice to stretch the selection box to the correct size without warping.
@@ -2219,18 +2227,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     add(gridGhostHoldNote);
     gridGhostHoldNote.zIndex = 11;
 
-    while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2))
-    {
-      var ghost = new ChartEditorHoldNoteSprite(this);
-      ghost.alpha = 0.6;
-      ghost.noteData = null;
-      ghost.visible = false;
-      add(ghost);
-      ghost.zIndex = 11;
-
-      gridPlayheadGhostHoldNotes.push(ghost);
-    }
-
     gridGhostEvent = new ChartEditorEventSprite(this);
     gridGhostEvent.alpha = 0.6;
     gridGhostEvent.eventData = new SongEventData(-1, '', {});
@@ -2300,6 +2296,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     add(notePreviewViewport);
     notePreviewViewport.zIndex = 30;
 
+    notePreviewPlayhead = new FlxSprite().makeGraphic(2, 2, 0xFFFF0000);
+    notePreviewPlayhead.scrollFactor.set(0, 0);
+    notePreviewPlayhead.scale.set(notePreview.width / 2, 0.5); // Setting width does nothing.
+    notePreviewPlayhead.updateHitbox();
+    notePreviewPlayhead.x = notePreview.x;
+    notePreviewPlayhead.y = notePreview.y;
+    add(notePreviewPlayhead);
+    notePreviewPlayhead.zIndex = 31;
+
     setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
   }
 
@@ -2399,6 +2404,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     }
   }
 
+  function refreshNotePreviewPlayheadPosition():Void
+  {
+    if (notePreviewPlayhead == null) return;
+
+    notePreviewPlayhead.y = notePreview.y + (notePreview.height * ((scrollPositionInPixels + playheadPositionInPixels) / songLengthInPixels));
+  }
+
   /**
    * Builds the group that will hold all the notes.
    */
@@ -4194,7 +4206,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             gridGhostHoldNote.visible = true;
             gridGhostHoldNote.noteData = gridGhostNote.noteData;
             gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
-
             gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
 
             gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
@@ -4741,27 +4752,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         // Do nothing.
     }
 
-    // Place notes at the playhead with the gamepad.
-    if (FlxG.gamepads.firstActive != null)
-    {
-      if (FlxG.gamepads.firstActive.justPressed.DPAD_LEFT) placeNoteAtPlayhead(4);
-      if (FlxG.gamepads.firstActive.justReleased.DPAD_LEFT) finishPlaceNoteAtPlayhead(4);
-      if (FlxG.gamepads.firstActive.justPressed.DPAD_DOWN) placeNoteAtPlayhead(5);
-      if (FlxG.gamepads.firstActive.justReleased.DPAD_DOWN) finishPlaceNoteAtPlayhead(5);
-      if (FlxG.gamepads.firstActive.justPressed.DPAD_UP) placeNoteAtPlayhead(6);
-      if (FlxG.gamepads.firstActive.justReleased.DPAD_UP) finishPlaceNoteAtPlayhead(6);
-      if (FlxG.gamepads.firstActive.justPressed.DPAD_RIGHT) placeNoteAtPlayhead(7);
-      if (FlxG.gamepads.firstActive.justReleased.DPAD_RIGHT) finishPlaceNoteAtPlayhead(7);
-
-      if (FlxG.gamepads.firstActive.justPressed.X) placeNoteAtPlayhead(0);
-      if (FlxG.gamepads.firstActive.justReleased.X) finishPlaceNoteAtPlayhead(0);
-      if (FlxG.gamepads.firstActive.justPressed.A) placeNoteAtPlayhead(1);
-      if (FlxG.gamepads.firstActive.justReleased.A) finishPlaceNoteAtPlayhead(1);
-      if (FlxG.gamepads.firstActive.justPressed.Y) placeNoteAtPlayhead(2);
-      if (FlxG.gamepads.firstActive.justReleased.Y) finishPlaceNoteAtPlayhead(2);
-      if (FlxG.gamepads.firstActive.justPressed.B) placeNoteAtPlayhead(3);
-      if (FlxG.gamepads.firstActive.justReleased.B) finishPlaceNoteAtPlayhead(3);
-    }
+    updatePlayheadGhostHoldNotes();
   }
 
   function placeNoteAtPlayhead(column:Int):Void
@@ -4781,38 +4772,68 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     if (notesAtPos.length == 0 && !removeNoteInstead)
     {
+      trace('Placing note. ${column}');
       var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
       performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
       currentLiveInputPlaceNoteData[column] = newNoteData;
-      gridPlayheadGhostHoldNotes[column].noteData = newNoteData.clone();
-      gridPlayheadGhostHoldNotes[column].noteDirection = newNoteData.getDirection();
     }
     else if (removeNoteInstead)
     {
-      trace('Removing existing note at position.');
+      trace('Removing existing note at position. ${column}');
       performCommand(new RemoveNotesCommand(notesAtPos));
     }
     else
     {
-      trace('Already a note there.');
+      trace('Already a note there. ${column}');
     }
   }
 
   function updatePlayheadGhostHoldNotes():Void
   {
-    // Update playhead ghost hold notes.
-    for (index in 0...gridPlayheadGhostHoldNotes.length)
+    // Ensure all the ghost hold notes exist.
+    while (gridPlayheadGhostHoldNotes.length < (STRUMLINE_SIZE * 2))
     {
-      var ghostHold = gridPlayheadGhostHoldNotes[index];
-      if (ghostHold == null) continue;
+      var ghost = new ChartEditorHoldNoteSprite(this);
+      ghost.alpha = 0.6;
+      ghost.noteData = null;
+      ghost.visible = false;
+      ghost.zIndex = 11;
+      add(ghost); // Don't add to `renderedHoldNotes` because then it will get killed every frame.
+
+      gridPlayheadGhostHoldNotes.push(ghost);
+      refresh();
+    }
+
+    // Update playhead ghost hold notes.
+    for (column in 0...gridPlayheadGhostHoldNotes.length)
+    {
+      var targetNoteData = currentLiveInputPlaceNoteData[column];
+      var ghostHold = gridPlayheadGhostHoldNotes[column];
+
+      if (targetNoteData == null && ghostHold.noteData != null)
+      {
+        // Remove the ghost hold note.
+        ghostHold.noteData = null;
+      }
+
+      if (targetNoteData != null && ghostHold.noteData == null)
+      {
+        // Readd the new ghost hold note.
+        ghostHold.noteData = targetNoteData.clone();
+        ghostHold.noteDirection = ghostHold.noteData.getDirection();
+        ghostHold.visible = true;
+        ghostHold.alpha = 0.6;
+        ghostHold.setHeightDirectly(0);
+        ghostHold.updateHoldNotePosition(renderedHoldNotes);
+      }
 
       if (ghostHold.noteData == null)
       {
         ghostHold.visible = false;
         ghostHold.setHeightDirectly(0);
-        playheadDragLengthCurrent[index] = 0;
+        playheadDragLengthCurrent[column] = 0;
         continue;
-      };
+      }
 
       var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
       var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
@@ -4829,22 +4850,25 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         var targetNoteLengthStepsInt:Int = Std.int(Math.floor(targetNoteLengthSteps));
         var targetNoteLengthPixels:Float = targetNoteLengthSteps * GRID_SIZE;
 
-        if (playheadDragLengthCurrent[index] != targetNoteLengthStepsInt)
+        if (playheadDragLengthCurrent[column] != targetNoteLengthStepsInt)
         {
           stretchySounds = !stretchySounds;
           this.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
-          playheadDragLengthCurrent[index] = targetNoteLengthStepsInt;
+          playheadDragLengthCurrent[column] = targetNoteLengthStepsInt;
         }
         ghostHold.visible = true;
-        trace('newHeight: ${targetNoteLengthPixels}');
+        ghostHold.alpha = 0.6;
         ghostHold.setHeightDirectly(targetNoteLengthPixels, true);
         ghostHold.updateHoldNotePosition(renderedHoldNotes);
+        trace('lerpLength: ${ghostHold.fullSustainLength}');
+        trace('position: ${ghostHold.x}, ${ghostHold.y}');
       }
       else
       {
         ghostHold.visible = false;
         ghostHold.setHeightDirectly(0);
-        playheadDragLengthCurrent[index] = 0;
+        playheadDragLengthCurrent[column] = 0;
+        continue;
       }
     }
   }
@@ -4864,14 +4888,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (newNoteLength < Conductor.instance.stepLengthMs)
     {
       // Don't extend the note if it's too short.
-      trace('Not extending note.');
+      trace('Not extending note. ${column}');
       currentLiveInputPlaceNoteData[column] = null;
       gridPlayheadGhostHoldNotes[column].noteData = null;
     }
     else
     {
       // Extend the note to the playhead position.
-      trace('Extending note.');
+      trace('Extending note. ${column}');
       this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
       performCommand(new ExtendNoteLengthCommand(currentLiveInputPlaceNoteData[column], newNoteLength));
       currentLiveInputPlaceNoteData[column] = null;
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx
index 896e2df68..70383d3fd 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorGamepadHandler.hx
@@ -1,133 +1,193 @@
 package funkin.ui.debug.charting.handlers;
 
+import haxe.ui.focus.FocusManager;
+import flixel.input.gamepad.FlxGamepad;
+import haxe.ui.actions.ActionManager;
+import haxe.ui.actions.IActionInputSource;
+import haxe.ui.actions.ActionType;
+
 /**
  * Yes, we're that crazy. Gamepad support for the chart editor.
  */
-@:nullSafety
+// @:nullSafety
+
 @:access(funkin.ui.debug.charting.ChartEditorState)
 class ChartEditorGamepadHandler
 {
   public static function handleGamepadControls(chartEditorState:ChartEditorState)
   {
-    if (FlxG.gamepads.firstActive == null) return;
+    if (FlxG.gamepads.firstActive != null) handleGamepad(chartEditorState, FlxG.gamepads.firstActive);
+  }
 
-    if (FlxG.gamepads.firstActive.justPressed.A)
+  /**
+   * Handle context-generic binds for the gamepad.
+   * @param chartEditorState The chart editor state.
+   * @param gamepad The gamepad to handle.
+   */
+  static function handleGamepad(chartEditorState:ChartEditorState, gamepad:FlxGamepad):Void
+  {
+    if (chartEditorState.isHaxeUIFocused)
     {
-      // trace('Gamepad: A pressed');
+      ChartEditorGamepadActionInputSource.instance.handleGamepad(gamepad);
     }
-    if (FlxG.gamepads.firstActive.justPressed.B)
+    else
     {
-      // trace('Gamepad: B pressed');
-    }
-    if (FlxG.gamepads.firstActive.justPressed.X)
-    {
-      // trace('Gamepad: X pressed');
-    }
-    if (FlxG.gamepads.firstActive.justPressed.Y)
-    {
-      // trace('Gamepad: Y pressed');
+      handleGamepadLiveInputs(chartEditorState, gamepad);
+
+      if (gamepad.justPressed.RIGHT_SHOULDER)
+      {
+        trace('Gamepad: Right shoulder pressed, toggling audio playback.');
+        chartEditorState.toggleAudioPlayback();
+      }
+
+      if (gamepad.justPressed.START)
+      {
+        var minimal = gamepad.pressed.LEFT_SHOULDER;
+        chartEditorState.hideAllToolboxes();
+        trace('Gamepad: Start pressed, opening playtest (minimal: ${minimal})');
+        chartEditorState.testSongInPlayState(minimal);
+      }
+
+      if (gamepad.justPressed.BACK && !gamepad.pressed.LEFT_SHOULDER)
+      {
+        trace('Gamepad: Back pressed, focusing on HaxeUI menu.');
+        // FocusManager.instance.focus = chartEditorState.menubarMenuFile;
+      }
+      else if (gamepad.justPressed.BACK && gamepad.pressed.LEFT_SHOULDER)
+      {
+        trace('Gamepad: Back pressed, unfocusing on HaxeUI menu.');
+        FocusManager.instance.focus = null;
+      }
     }
 
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_SHOULDER)
+    if (gamepad.justPressed.GUIDE)
     {
-      // trace('Gamepad: LEFT_SHOULDER pressed');
-    }
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_SHOULDER)
-    {
-      // trace('Gamepad: RIGHT_SHOULDER pressed');
+      trace('Gamepad: Guide pressed, quitting chart editor.');
+      chartEditorState.quitChartEditor();
     }
+  }
 
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_STICK_CLICK)
+  static function handleGamepadLiveInputs(chartEditorState:ChartEditorState, gamepad:FlxGamepad):Void
+  {
+    // Place notes at the playhead with the gamepad.
+    // Disable when we are interacting with HaxeUI.
+    if (!(chartEditorState.isHaxeUIFocused || chartEditorState.isHaxeUIDialogOpen))
     {
-      // trace('Gamepad: LEFT_STICK_CLICK pressed');
-    }
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_STICK_CLICK)
-    {
-      // trace('Gamepad: RIGHT_STICK_CLICK pressed');
-    }
+      if (gamepad.justPressed.DPAD_LEFT) chartEditorState.placeNoteAtPlayhead(4);
+      if (gamepad.justReleased.DPAD_LEFT) chartEditorState.finishPlaceNoteAtPlayhead(4);
+      if (gamepad.justPressed.DPAD_DOWN) chartEditorState.placeNoteAtPlayhead(5);
+      if (gamepad.justReleased.DPAD_DOWN) chartEditorState.finishPlaceNoteAtPlayhead(5);
+      if (gamepad.justPressed.DPAD_UP) chartEditorState.placeNoteAtPlayhead(6);
+      if (gamepad.justReleased.DPAD_UP) chartEditorState.finishPlaceNoteAtPlayhead(6);
+      if (gamepad.justPressed.DPAD_RIGHT) chartEditorState.placeNoteAtPlayhead(7);
+      if (gamepad.justReleased.DPAD_RIGHT) chartEditorState.finishPlaceNoteAtPlayhead(7);
 
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_TRIGGER)
-    {
-      // trace('Gamepad: LEFT_TRIGGER pressed');
-    }
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_TRIGGER)
-    {
-      // trace('Gamepad: RIGHT_TRIGGER pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.START)
-    {
-      // trace('Gamepad: START pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.BACK)
-    {
-      // trace('Gamepad: BACK pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.GUIDE)
-    {
-      // trace('Gamepad: GUIDE pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.DPAD_UP)
-    {
-      // trace('Gamepad: DPAD_UP pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.DPAD_DOWN)
-    {
-      // trace('Gamepad: DPAD_DOWN pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.DPAD_LEFT)
-    {
-      // trace('Gamepad: DPAD_LEFT pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.DPAD_RIGHT)
-    {
-      // trace('Gamepad: DPAD_RIGHT pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_STICK_DIGITAL_UP)
-    {
-      // trace('Gamepad: LEFT_STICK_DIGITAL_UP pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_STICK_DIGITAL_DOWN)
-    {
-      // trace('Gamepad: LEFT_STICK_DIGITAL_DOWN pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_STICK_DIGITAL_LEFT)
-    {
-      // trace('Gamepad: LEFT_STICK_DIGITAL_LEFT pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.LEFT_STICK_DIGITAL_RIGHT)
-    {
-      // trace('Gamepad: LEFT_STICK_DIGITAL_RIGHT pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_STICK_DIGITAL_UP)
-    {
-      // trace('Gamepad: RIGHT_STICK_DIGITAL_UP pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_STICK_DIGITAL_DOWN)
-    {
-      // trace('Gamepad: RIGHT_STICK_DIGITAL_DOWN pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_STICK_DIGITAL_LEFT)
-    {
-      // trace('Gamepad: RIGHT_STICK_DIGITAL_LEFT pressed');
-    }
-
-    if (FlxG.gamepads.firstActive.justPressed.RIGHT_STICK_DIGITAL_RIGHT)
-    {
-      // trace('Gamepad: RIGHT_STICK_DIGITAL_RIGHT pressed');
+      if (gamepad.justPressed.X) chartEditorState.placeNoteAtPlayhead(0);
+      if (gamepad.justReleased.X) chartEditorState.finishPlaceNoteAtPlayhead(0);
+      if (gamepad.justPressed.A) chartEditorState.placeNoteAtPlayhead(1);
+      if (gamepad.justReleased.A) chartEditorState.finishPlaceNoteAtPlayhead(1);
+      if (gamepad.justPressed.Y) chartEditorState.placeNoteAtPlayhead(2);
+      if (gamepad.justReleased.Y) chartEditorState.finishPlaceNoteAtPlayhead(2);
+      if (gamepad.justPressed.B) chartEditorState.placeNoteAtPlayhead(3);
+      if (gamepad.justReleased.B) chartEditorState.finishPlaceNoteAtPlayhead(3);
+    }
+  }
+}
+
+class ChartEditorGamepadActionInputSource implements IActionInputSource
+{
+  public static var instance:ChartEditorGamepadActionInputSource = new ChartEditorGamepadActionInputSource();
+
+  public function new() {}
+
+  public function start():Void {}
+
+  /**
+   * Handle HaxeUI-specific binds for the gamepad.
+   * Only called when the HaxeUI menu is focused.
+   * @param chartEditorState The chart editor state.
+   * @param gamepad The gamepad to handle.
+   */
+  public function handleGamepad(gamepad:FlxGamepad):Void
+  {
+    if (gamepad.justPressed.DPAD_LEFT)
+    {
+      trace('Gamepad: DPAD_LEFT pressed, moving left.');
+      ActionManager.instance.actionStart(ActionType.LEFT, this);
+    }
+    else if (gamepad.justReleased.DPAD_LEFT)
+    {
+      ActionManager.instance.actionEnd(ActionType.LEFT, this);
+    }
+
+    if (gamepad.justPressed.DPAD_RIGHT)
+    {
+      trace('Gamepad: DPAD_RIGHT pressed, moving right.');
+      ActionManager.instance.actionStart(ActionType.RIGHT, this);
+    }
+    else if (gamepad.justReleased.DPAD_RIGHT)
+    {
+      ActionManager.instance.actionEnd(ActionType.RIGHT, this);
+    }
+
+    if (gamepad.justPressed.DPAD_UP)
+    {
+      trace('Gamepad: DPAD_UP pressed, moving up.');
+      ActionManager.instance.actionStart(ActionType.UP, this);
+    }
+    else if (gamepad.justReleased.DPAD_UP)
+    {
+      ActionManager.instance.actionEnd(ActionType.UP, this);
+    }
+
+    if (gamepad.justPressed.DPAD_DOWN)
+    {
+      trace('Gamepad: DPAD_DOWN pressed, moving down.');
+      ActionManager.instance.actionStart(ActionType.DOWN, this);
+    }
+    else if (gamepad.justReleased.DPAD_DOWN)
+    {
+      ActionManager.instance.actionEnd(ActionType.DOWN, this);
+    }
+
+    if (gamepad.justPressed.A)
+    {
+      trace('Gamepad: A pressed, confirmingg.');
+      ActionManager.instance.actionStart(ActionType.CONFIRM, this);
+    }
+    else if (gamepad.justReleased.A)
+    {
+      ActionManager.instance.actionEnd(ActionType.CONFIRM, this);
+    }
+
+    if (gamepad.justPressed.B)
+    {
+      trace('Gamepad: B pressed, cancelling.');
+      ActionManager.instance.actionStart(ActionType.CANCEL, this);
+    }
+    else if (gamepad.justReleased.B)
+    {
+      ActionManager.instance.actionEnd(ActionType.CANCEL, this);
+    }
+
+    if (gamepad.justPressed.LEFT_TRIGGER)
+    {
+      trace('Gamepad: LEFT_TRIGGER pressed, moving to previous item.');
+      ActionManager.instance.actionStart(ActionType.PREVIOUS, this);
+    }
+    else if (gamepad.justReleased.LEFT_TRIGGER)
+    {
+      ActionManager.instance.actionEnd(ActionType.PREVIOUS, this);
+    }
+
+    if (gamepad.justPressed.RIGHT_TRIGGER)
+    {
+      trace('Gamepad: RIGHT_TRIGGER pressed, moving to next item.');
+      ActionManager.instance.actionStart(ActionType.NEXT, this);
+    }
+    else if (gamepad.justReleased.RIGHT_TRIGGER)
+    {
+      ActionManager.instance.actionEnd(ActionType.NEXT, this);
     }
   }
 }
diff --git a/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx b/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx
new file mode 100644
index 000000000..9c2901d16
--- /dev/null
+++ b/source/funkin/ui/haxeui/FlxGamepadActionInputSource.hx
@@ -0,0 +1,53 @@
+package funkin.ui.haxeui;
+
+import flixel.FlxBasic;
+import flixel.input.gamepad.FlxGamepad;
+import haxe.ui.actions.IActionInputSource;
+
+/**
+ * Receives button presses from the Flixel gamepad and emits HaxeUI events.
+ */
+class FlxGamepadActionInputSource extends FlxBasic
+{
+  public static var instance(get, null):FlxGamepadActionInputSource;
+
+  static function get_instance():FlxGamepadActionInputSource
+  {
+    if (instance == null) instance = new FlxGamepadActionInputSource();
+    return instance;
+  }
+
+  public function new()
+  {
+    super();
+  }
+
+  public function start():Void
+  {
+    FlxG.plugins.addPlugin(this);
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (FlxG.gamepads.firstActive != null)
+    {
+      updateGamepad(elapsed, FlxG.gamepads.firstActive);
+    }
+  }
+
+  function updateGamepad(elapsed:Float, gamepad:FlxGamepad):Void
+  {
+    if (gamepad.justPressed.BACK)
+    {
+      //
+    }
+  }
+
+  public override function destroy():Void
+  {
+    super.destroy();
+    FlxG.plugins.remove(this);
+  }
+}