diff --git a/js/components/MilkdropWindow/PresetOverlay.js b/js/components/MilkdropWindow/PresetOverlay.js
index 7bc97d22..6b98b31c 100644
--- a/js/components/MilkdropWindow/PresetOverlay.js
+++ b/js/components/MilkdropWindow/PresetOverlay.js
@@ -1,10 +1,59 @@
import React from "react";
import { promptForFileReferences } from "../../fileUtils";
+import { clamp } from "../../utils";
+
+const ENTRY_HEIGHT = 14;
+const HEIGHT_PADDING = 15;
+const WIDTH_PADDING = 20;
+
+const LoadingState = () => (
+
+ Loading presets
+
+);
+
+const ListWrapper = ({ width, height, children }) => (
+
+);
class PresetOverlay extends React.Component {
constructor(props) {
super(props);
- this.state = { presetIdx: Math.max(props.currentPreset, 0) };
+ const listIndex = this._listIndexFromPresetIndex(props.currentPreset);
+ this.state = {
+ selectedListIndex: clamp(listIndex, 0, this._maxListIndex())
+ };
}
componentDidMount() {
@@ -22,23 +71,27 @@ class PresetOverlay extends React.Component {
_handleFocusedKeyboardInput = e => {
switch (e.keyCode) {
case 38: // up arrow
- this.setState({ presetIdx: Math.max(this.state.presetIdx - 1, -1) });
+ this.setState({
+ selectedListIndex: Math.max(this.state.selectedListIndex - 1, 0)
+ });
e.stopPropagation();
break;
case 40: // down arrow
this.setState({
- presetIdx: Math.min(
- this.state.presetIdx + 1,
- this.props.presetKeys.length - 1
+ selectedListIndex: Math.min(
+ this.state.selectedListIndex + 1,
+ this._maxListIndex()
)
});
e.stopPropagation();
break;
case 13: // enter
- if (this.state.presetIdx === -1) {
+ if (this.state.selectedListIndex === 0) {
this.loadLocalDir();
} else {
- this.props.selectPreset(this.state.presetIdx);
+ this.props.selectPreset(
+ this._presetIndexFromListIndex(this.state.selectedListIndex)
+ );
}
e.stopPropagation();
break;
@@ -49,103 +102,83 @@ class PresetOverlay extends React.Component {
}
};
+ _presetIndexFromListIndex(listIndex) {
+ return listIndex - 1;
+ }
+
+ _listIndexFromPresetIndex(listIndex) {
+ return listIndex + 1;
+ }
+
+ _maxListIndex() {
+ // Number of presets, plus one for the "Load Local Directory" option, minus
+ // one to convert a length to an index.
+ return this.props.presetKeys.length; // - 1 + 1;
+ }
+
async loadLocalDir() {
const fileReferences = await promptForFileReferences({ directory: true });
+ // TODO: Technically there is a race condition here, since the component
+ // could get unmounted before the promise resolves.
this.props.loadPresets(fileReferences);
}
- render() {
- if (!this.props.presetKeys) {
- return (
-
- Loading presets
-
- );
- }
+ _renderList() {
+ const { presetKeys, currentPreset, height, width } = this.props;
+ const { selectedListIndex } = this.state;
- // display highlighted preset in the middle if possible
- const numPresets = this.props.presetKeys.length;
- let presetListLen = Math.floor(this.props.height / 20);
- presetListLen = Math.min(Math.max(presetListLen, 3), numPresets);
- presetListLen = presetListLen % 2 ? presetListLen : presetListLen - 1;
- const halfPresetListLen = Math.floor(presetListLen / 2);
- let startIdx = Math.max(this.state.presetIdx - halfPresetListLen, -1);
- let endIdx = Math.min(startIdx + presetListLen, numPresets);
- if (endIdx >= numPresets) {
- startIdx = Math.max(endIdx - presetListLen, -1);
- endIdx = Math.min(startIdx + presetListLen, numPresets);
- }
- startIdx = Math.max(startIdx, 0); // ensure startIdx >= 0 after endIdx is calculated
- const presets = this.props.presetKeys.slice(startIdx, endIdx);
- const presetElms = presets.map((presetName, i) => {
- let color;
- if (i + startIdx === this.props.currentPreset) {
- if (i + startIdx === this.state.presetIdx) {
- color = "#FFCC22";
- } else {
- color = "#CCFF03";
- }
- } else if (i + startIdx === this.state.presetIdx) {
- color = "#FF5050";
- } else {
- color = "#CCCCCC";
- }
- return (
-
- {presetName}
-
- );
- });
+ const maxVisibleRows = Math.floor((height - HEIGHT_PADDING) / ENTRY_HEIGHT);
+ const rowsToShow = Math.floor(maxVisibleRows * 0.75); // Only fill 3/4 of the screen.
+ const [startIndex, endIndex] = getRangeCenteredOnIndex(
+ this._maxListIndex() + 1, // Add one to convert an index to a length
+ rowsToShow,
+ selectedListIndex
+ );
- if (this.state.presetIdx - halfPresetListLen < 0) {
+ const presetElms = [];
+ for (let i = startIndex; i <= endIndex; i++) {
+ const presetIndex = this._presetIndexFromListIndex(i);
+ const isSelected = i === selectedListIndex;
+ const isCurrent = presetIndex === currentPreset;
let color;
- if (this.state.presetIdx === -1) {
- color = "#FF5050";
+ if (isSelected) {
+ color = isCurrent ? "#FFCC22" : "#FF5050";
} else {
- color = "#CCCCCC";
+ color = isCurrent ? "#CCFF03" : "#CCCCCC";
}
- presetElms.unshift(
-
- Load Local Directory
+ presetElms.push(
+
+ {i === 0 ? "Load Local Directory" : presetKeys[presetIndex]}
);
}
return (
-
+
+ {presetElms}
+
+ );
+ }
+
+ render() {
+ return this.props.presetKeys != null ? (
+ this._renderList()
+ ) : (
+
);
}
}
+// Find a tuple `[startIndex, endIndex]` representing start/end indexes into an
+// array of length `length`, that descripe a range of size up to `rangeSize`
+// where a best effort is made to center `indexToCenter`.
+export function getRangeCenteredOnIndex(length, maxRangeSize, indexToCenter) {
+ const rangeSize = Math.min(length, maxRangeSize);
+ const halfRangeSize = Math.floor(rangeSize / 2);
+ const idealStartIndex = indexToCenter - halfRangeSize;
+ const startIndex = clamp(idealStartIndex, 0, length - rangeSize);
+ const endIndex = startIndex + rangeSize - 1;
+ return [startIndex, endIndex];
+}
+
export default PresetOverlay;
diff --git a/js/components/MilkdropWindow/__tests__/PresetOverlay.test.js b/js/components/MilkdropWindow/__tests__/PresetOverlay.test.js
new file mode 100644
index 00000000..488d4b84
--- /dev/null
+++ b/js/components/MilkdropWindow/__tests__/PresetOverlay.test.js
@@ -0,0 +1,29 @@
+import { getRangeCenteredOnIndex } from "../PresetOverlay";
+
+describe("getRangeCenteredOnIndex", () => {
+ test("gets the whole range when the list is the same size as the range", () => {
+ expect(getRangeCenteredOnIndex(100, 100, 0)).toEqual([0, 99]);
+ expect(getRangeCenteredOnIndex(100, 100, 99)).toEqual([0, 99]);
+ expect(getRangeCenteredOnIndex(100, 100, 50)).toEqual([0, 99]);
+ });
+ test("gets the first elements when index < half of range", () => {
+ expect(getRangeCenteredOnIndex(100, 50, 0)).toEqual([0, 49]);
+ expect(getRangeCenteredOnIndex(100, 50, 24)).toEqual([0, 49]);
+ });
+ test("truncates the returned range when the passed range is smaller than items", () => {
+ expect(getRangeCenteredOnIndex(100, 50, 0)).toEqual([0, 49]);
+ expect(getRangeCenteredOnIndex(100, 50, 24)).toEqual([0, 49]);
+ });
+
+ test("follows an example sliding window", () => {
+ expect(getRangeCenteredOnIndex(10, 5, 1)).toEqual([0, 4]);
+ expect(getRangeCenteredOnIndex(10, 5, 2)).toEqual([0, 4]);
+ expect(getRangeCenteredOnIndex(10, 5, 3)).toEqual([1, 5]);
+ expect(getRangeCenteredOnIndex(10, 5, 4)).toEqual([2, 6]);
+ expect(getRangeCenteredOnIndex(10, 5, 5)).toEqual([3, 7]);
+ expect(getRangeCenteredOnIndex(10, 5, 6)).toEqual([4, 8]);
+ expect(getRangeCenteredOnIndex(10, 5, 7)).toEqual([5, 9]);
+ expect(getRangeCenteredOnIndex(10, 5, 8)).toEqual([5, 9]);
+ expect(getRangeCenteredOnIndex(10, 5, 9)).toEqual([5, 9]);
+ });
+});