Funkin/source/funkin/ui/MenuList.hx
2023-06-08 16:30:45 -04:00

359 lines
8.2 KiB
Haxe

package funkin.ui;
import flixel.FlxSprite;
import flixel.effects.FlxFlicker;
import flixel.group.FlxGroup;
import flixel.math.FlxPoint;
import flixel.util.FlxSignal;
class MenuTypedList<T:MenuItem> extends FlxTypedGroup<T>
{
public var selectedIndex(default, null) = 0;
public var selectedItem(get, never):T;
/** Called when a new item is highlighted */
public var onChange(default, null) = new FlxTypedSignal<T->Void>();
/** Called when an item is accepted */
public var onAcceptPress(default, null) = new FlxTypedSignal<T->Void>();
/** The navigation control scheme to use */
public var navControls:NavControls;
/** Set to false to disable nav control */
public var enabled:Bool = true;
/** */
public var wrapMode:WrapMode = Both;
var byName = new Map<String, T>();
/** Set to true, internally to disable controls, without affecting vars like `enabled` */
public var busy(default, null):Bool = false;
// bit awkward because BACK is also a menu control and this doesn't affect that
public function new(navControls:NavControls = Vertical, ?wrapMode:WrapMode)
{
this.navControls = navControls;
if (wrapMode != null) this.wrapMode = wrapMode;
else
this.wrapMode = switch (navControls)
{
case Horizontal: Horizontal;
case Vertical: Vertical;
default: Both;
}
super();
}
public function addItem(name:String, item:T):T
{
if (length == selectedIndex) item.select();
byName[name] = item;
return add(item);
}
public function resetItem(oldName:String, newName:String, ?callback:Void->Void):T
{
if (!byName.exists(oldName)) throw "No item named:" + oldName;
var item = byName[oldName];
byName.remove(oldName);
byName[newName] = item;
item.setItem(newName, callback);
return item;
}
override function update(elapsed:Float)
{
super.update(elapsed);
if (enabled && !busy) updateControls();
}
inline function updateControls()
{
var controls = PlayerSettings.player1.controls;
var wrapX = wrapMode.match(Horizontal | Both);
var wrapY = wrapMode.match(Vertical | Both);
var newIndex = switch (navControls)
{
case Vertical: navList(controls.UI_UP_P, controls.UI_DOWN_P, wrapY);
case Horizontal: navList(controls.UI_LEFT_P, controls.UI_RIGHT_P, wrapX);
case Both: navList(controls.UI_LEFT_P || controls.UI_UP_P, controls.UI_RIGHT_P || controls.UI_DOWN_P, !wrapMode.match(None));
case Columns(num): navGrid(num, controls.UI_LEFT_P, controls.UI_RIGHT_P, wrapX, controls.UI_UP_P, controls.UI_DOWN_P, wrapY);
case Rows(num): navGrid(num, controls.UI_UP_P, controls.UI_DOWN_P, wrapY, controls.UI_LEFT_P, controls.UI_RIGHT_P, wrapX);
}
if (newIndex != selectedIndex)
{
FlxG.sound.play(Paths.sound('scrollMenu'));
selectItem(newIndex);
}
// Todo: bypass popup blocker on firefox
if (controls.ACCEPT) accept();
}
function navAxis(index:Int, size:Int, prev:Bool, next:Bool, allowWrap:Bool):Int
{
if (prev == next) return index;
if (prev)
{
if (index > 0) index--;
else if (allowWrap) index = size - 1;
}
else
{
if (index < size - 1) index++;
else if (allowWrap) index = 0;
}
return index;
}
/**
* Controls navigation on a linear list of items such as Vertical.
* @param prev
* @param next
* @param allowWrap
*/
inline function navList(prev:Bool, next:Bool, allowWrap:Bool)
{
return navAxis(selectedIndex, length, prev, next, allowWrap);
}
/**
* Controls navigation on a grid
* @param latSize The size of the fixed axis of the grid, or the "lateral axis"
* @param latPrev Whether the 'prev' key is pressed along the fixed-lengthed axis. eg: "left" in Column mode
* @param latNext Whether the 'next' key is pressed along the fixed-lengthed axis. eg: "right" in Column mode
* @param prev Whether the 'prev' key is pressed along the variable-lengthed axis. eg: "up" in Column mode
* @param next Whether the 'next' key is pressed along the variable-lengthed axis. eg: "down" in Column mode
* @param allowWrap unused
*/
function navGrid(latSize:Int, latPrev:Bool, latNext:Bool, latAllowWrap:Bool, prev:Bool, next:Bool, allowWrap:Bool):Int
{
// The grid lenth along the variable-length axis
var size = Math.ceil(length / latSize);
// The selected position along the variable-length axis
var index = Math.floor(selectedIndex / latSize);
// The selected position along the fixed axis
var latIndex = selectedIndex % latSize;
latIndex = navAxis(latIndex, latSize, latPrev, latNext, latAllowWrap);
index = navAxis(index, size, prev, next, allowWrap);
return Std.int(Math.min(length - 1, index * latSize + latIndex));
}
public function accept()
{
var selected = members[selectedIndex];
onAcceptPress.dispatch(selected);
if (selected.fireInstantly) selected.callback();
else
{
busy = true;
FlxG.sound.play(Paths.sound('confirmMenu'));
FlxFlicker.flicker(selected, 1, 0.06, true, false, function(_) {
busy = false;
selected.callback();
});
}
}
public function selectItem(index:Int)
{
members[selectedIndex].idle();
selectedIndex = index;
var selected = members[selectedIndex];
selected.select();
onChange.dispatch(selected);
}
public function has(name:String)
{
return byName.exists(name);
}
public function getItem(name:String)
{
return byName[name];
}
override function destroy()
{
super.destroy();
byName.clear();
onChange.removeAll();
onAcceptPress.removeAll();
}
inline function get_selectedItem():T
{
return members[selectedIndex];
}
}
class MenuItem extends FlxSprite
{
public var callback:Void->Void;
public var name:String;
/**
* Set to true for things like opening URLs otherwise, it may it get blocked.
*/
public var fireInstantly = false;
public var selected(get, never):Bool;
function get_selected()
return alpha == 1.0;
public function new(x = 0.0, y = 0.0, name:String, callback)
{
super(x, y);
antialiasing = true;
setData(name, callback);
idle();
}
function setData(name:String, ?callback:Void->Void)
{
this.name = name;
if (callback != null) this.callback = callback;
}
/**
* Calls setData and resets/redraws the state of the item
* @param name the label.
* @param callback Unchanged if null.
*/
public function setItem(name:String, ?callback:Void->Void)
{
setData(name, callback);
if (selected) select();
else
idle();
}
public function idle()
{
alpha = 0.6;
}
public function select()
{
alpha = 1.0;
}
}
class MenuTypedItem<T:FlxSprite> extends MenuItem
{
public var label(default, set):T;
public function new(x = 0.0, y = 0.0, label:T, name:String, callback)
{
super(x, y, name, callback);
// set label after super otherwise setters fuck up
this.label = label;
}
/**
* Use this when you only want to show the label
*/
function setEmptyBackground()
{
var oldWidth = width;
var oldHeight = height;
makeGraphic(1, 1, 0x0);
width = oldWidth;
height = oldHeight;
}
function set_label(value:T)
{
if (value != null)
{
value.x = x;
value.y = y;
value.alpha = alpha;
}
return this.label = value;
}
override function update(elapsed:Float)
{
super.update(elapsed);
if (label != null) label.update(elapsed);
}
override function draw()
{
super.draw();
if (label != null)
{
label.cameras = cameras;
label.scrollFactor.copyFrom(scrollFactor);
label.draw();
}
}
override function set_alpha(value:Float):Float
{
super.set_alpha(value);
if (label != null) label.alpha = alpha;
return alpha;
}
override function set_x(value:Float):Float
{
super.set_x(value);
if (label != null) label.x = x;
return x;
}
override function set_y(Value:Float):Float
{
super.set_y(Value);
if (label != null) label.y = y;
return y;
}
}
enum NavControls
{
Horizontal;
Vertical;
Both;
Columns(num:Int);
Rows(num:Int);
}
enum WrapMode
{
Horizontal;
Vertical;
Both;
None;
}