From 26cccd84c1d681bb4bb972cdedb792c23979cab3 Mon Sep 17 00:00:00 2001
From: Letter N <24603524+LetterN@users.noreply.github.com>
Date: Tue, 21 Jul 2020 21:00:18 +0800
Subject: [PATCH] actual tgui4
---
tgui/packages/tgui/assets.js | 54 ++
tgui/packages/tgui/backend.js | 221 ++++++-
tgui/packages/tgui/components/Box.js | 23 +-
tgui/packages/tgui/components/Button.js | 8 +-
tgui/packages/tgui/components/ByondUi.js | 40 +-
tgui/packages/tgui/components/Chart.js | 3 +-
tgui/packages/tgui/components/Flex.js | 9 +-
tgui/packages/tgui/components/Input.js | 4 +
tgui/packages/tgui/components/Knob.js | 3 +-
tgui/packages/tgui/components/NumberInput.js | 3 +-
tgui/packages/tgui/components/Section.js | 15 +-
tgui/packages/tgui/components/Slider.js | 3 +-
tgui/packages/tgui/components/index.js | 3 +-
tgui/packages/tgui/debug/KitchenSink.js | 608 ++++++++++++++++++
tgui/packages/tgui/debug/index.js | 49 ++
tgui/packages/tgui/drag.js | 195 ++++--
tgui/packages/tgui/format.js | 3 +
tgui/packages/tgui/global.d.ts | 95 +++
tgui/packages/tgui/hotkeys.js | 105 +--
tgui/packages/tgui/index.js | 158 +++--
tgui/packages/tgui/layouts/Layout.js | 9 +-
tgui/packages/tgui/layouts/NtosWindow.js | 19 +-
tgui/packages/tgui/layouts/Window.js | 86 ++-
tgui/packages/tgui/logging.js | 10 +-
tgui/packages/tgui/package.json | 8 +-
tgui/packages/tgui/public/tgui.html | 152 ++++-
tgui/packages/tgui/routes.js | 21 +-
tgui/packages/tgui/store.js | 36 +-
tgui/packages/tgui/styles/atomic/outline.scss | 10 +-
tgui/packages/tgui/styles/base.scss | 13 +-
.../tgui/styles/components/BlockQuote.scss | 6 +-
.../tgui/styles/components/Button.scss | 16 +-
.../tgui/styles/components/ColorBox.scss | 6 +-
.../tgui/styles/components/Divider.scss | 6 +-
.../tgui/styles/components/Dropdown.scss | 32 +-
.../packages/tgui/styles/components/Flex.scss | 14 +-
.../tgui/styles/components/Input.scss | 20 +-
.../packages/tgui/styles/components/Knob.scss | 38 --
.../tgui/styles/components/LabeledList.scss | 12 +-
.../tgui/styles/components/NoticeBox.scss | 11 +-
.../tgui/styles/components/NumberInput.scss | 31 +-
.../tgui/styles/components/ProgressBar.scss | 14 +-
.../tgui/styles/components/Section.scss | 35 +-
.../tgui/styles/components/Slider.scss | 21 +-
.../tgui/styles/components/Table.scss | 4 +-
.../packages/tgui/styles/components/Tabs.scss | 17 +-
.../tgui/styles/components/TextArea.scss | 158 ++---
.../tgui/styles/components/Tooltip.scss | 30 +-
.../tgui/styles/interfaces/CameraConsole.scss | 27 +-
.../tgui/styles/interfaces/NuclearBomb.scss | 13 +-
.../tgui/styles/interfaces/Roulette.scss | 1 +
tgui/packages/tgui/styles/layouts/Layout.scss | 22 +-
.../tgui/styles/layouts/NtosHeader.scss | 8 +-
.../tgui/styles/layouts/NtosWindow.scss | 12 +-
.../tgui/styles/layouts/TitleBar.scss | 35 +-
tgui/packages/tgui/styles/layouts/Window.scss | 36 +-
tgui/packages/tgui/styles/main.scss | 4 +-
tgui/packages/tgui/styles/reset.scss | 9 +-
.../packages/tgui/styles/themes/abductor.scss | 59 ++
.../tgui/styles/themes/cardtable.scss | 8 +-
.../tgui/styles/themes/hackerman.scss | 5 +-
.../tgui/styles/themes/malfunction.scss | 1 -
tgui/packages/tgui/styles/themes/ntos.scss | 1 -
tgui/packages/tgui/styles/themes/paper.scss | 199 ++++--
tgui/packages/tgui/styles/themes/retro.scss | 8 +-
.../tgui/styles/themes/syndicate.scss | 1 -
66 files changed, 2192 insertions(+), 694 deletions(-)
create mode 100644 tgui/packages/tgui/assets.js
create mode 100644 tgui/packages/tgui/debug/KitchenSink.js
create mode 100644 tgui/packages/tgui/debug/index.js
create mode 100644 tgui/packages/tgui/global.d.ts
create mode 100644 tgui/packages/tgui/styles/themes/abductor.scss
diff --git a/tgui/packages/tgui/assets.js b/tgui/packages/tgui/assets.js
new file mode 100644
index 0000000000..b0f71bb9ff
--- /dev/null
+++ b/tgui/packages/tgui/assets.js
@@ -0,0 +1,54 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { loadCSS as fgLoadCSS } from 'fg-loadcss';
+import { createLogger } from './logging';
+
+const logger = createLogger('assets');
+
+const EXCLUDED_PATTERNS = [
+ /v4shim/i,
+];
+
+const loadedStyles = [];
+const loadedMappings = {};
+
+export const loadCSS = url => {
+ if (loadedStyles.includes(url)) {
+ return;
+ }
+ loadedStyles.push(url);
+ logger.log(`loading stylesheet '${url}'`);
+ fgLoadCSS(url);
+};
+
+export const resolveAsset = name => (
+ loadedMappings[name] || name
+);
+
+export const assetMiddleware = store => next => action => {
+ const { type, payload } = action;
+ if (type === 'asset/stylesheet') {
+ loadCSS(payload);
+ return;
+ }
+ if (type === 'asset/mappings') {
+ for (let name of Object.keys(payload)) {
+ // Skip anything that matches excluded patterns
+ if (EXCLUDED_PATTERNS.some(regex => regex.test(name))) {
+ continue;
+ }
+ const url = payload[name];
+ const ext = name.split('.').pop();
+ loadedMappings[name] = url;
+ if (ext === 'css') {
+ loadCSS(url);
+ }
+ }
+ return;
+ }
+ next(action);
+};
diff --git a/tgui/packages/tgui/backend.js b/tgui/packages/tgui/backend.js
index 090970fe50..dab86ee918 100644
--- a/tgui/packages/tgui/backend.js
+++ b/tgui/packages/tgui/backend.js
@@ -11,8 +11,12 @@
* @license MIT
*/
+import { perf } from 'common/perf';
import { UI_DISABLED, UI_INTERACTIVE } from './constants';
-import { callByond } from './byond';
+import { releaseHeldKeys } from './hotkeys';
+import { createLogger } from './logging';
+
+const logger = createLogger('backend');
export const backendUpdate = state => ({
type: 'backend/update',
@@ -24,7 +28,23 @@ export const backendSetSharedState = (key, nextState) => ({
payload: { key, nextState },
});
-export const backendReducer = (state, action) => {
+export const backendSuspendStart = () => ({
+ type: 'backend/suspendStart',
+});
+
+export const backendSuspendSuccess = () => ({
+ type: 'backend/suspendSuccess',
+ payload: {
+ timestamp: Date.now(),
+ },
+});
+
+const initialState = {
+ config: {},
+ data: {},
+};
+
+export const backendReducer = (state = initialState, action) => {
const { type, payload } = action;
if (type === 'backend/update') {
@@ -63,6 +83,7 @@ export const backendReducer = (state, action) => {
shared,
visible,
interactive,
+ suspended: false,
};
}
@@ -77,30 +98,175 @@ export const backendReducer = (state, action) => {
};
}
+ if (type === 'backend/suspendStart') {
+ return {
+ ...state,
+ suspending: true,
+ };
+ }
+
+ if (type === 'backend/suspendSuccess') {
+ const { timestamp } = payload;
+ return {
+ ...state,
+ data: {},
+ shared: {},
+ config: {
+ ...state.config,
+ title: '',
+ status: 1,
+ },
+ suspending: false,
+ suspended: timestamp,
+ };
+ }
+
return state;
};
+export const backendMiddleware = store => {
+ let fancyState;
+ let suspendInterval;
+
+ return next => action => {
+ const { config, suspended } = selectBackend(store.getState());
+ const { type, payload } = action;
+
+ if (type === 'backend/suspendStart' && !suspendInterval) {
+ logger.log(`suspending (${window.__windowId__})`);
+ // Keep sending suspend messages until it succeeds.
+ // It may fail multiple times due to topic rate limiting.
+ const suspendFn = () => sendMessage({
+ type: 'suspend',
+ });
+ suspendFn();
+ suspendInterval = setInterval(suspendFn, 2000);
+ }
+
+ if (type === 'backend/suspendSuccess') {
+ clearInterval(suspendInterval);
+ suspendInterval = undefined;
+ releaseHeldKeys();
+ Byond.winset(window.__windowId__, {
+ 'is-visible': false,
+ });
+ }
+
+ if (type === 'backend/update') {
+ const fancy = payload.config?.window?.fancy;
+ // Initialize fancy state
+ if (fancyState === undefined) {
+ fancyState = fancy;
+ }
+ // React to changes in fancy
+ else if (fancyState !== fancy) {
+ logger.log('changing fancy mode to', fancy);
+ fancyState = fancy;
+ Byond.winset(window.__windowId__, {
+ titlebar: !fancy,
+ 'can-resize': !fancy,
+ });
+ }
+ }
+
+ if (type === 'backend/update' && suspended) {
+ // We schedule this for the next tick here because resizing and unhiding
+ // during the same tick will flash with a white background.
+ setImmediate(() => {
+ perf.mark('resume/start');
+ // Doublecheck if we are not re-suspended.
+ const { suspended } = selectBackend(store.getState());
+ if (suspended) {
+ return;
+ }
+ Byond.winset(window.__windowId__, {
+ 'is-visible': true,
+ });
+ perf.mark('resume/finish');
+ if (process.env.NODE_ENV !== 'production') {
+ logger.log('visible in',
+ perf.measure('render/finish', 'resume/finish'));
+ }
+ });
+ }
+
+ return next(action);
+ };
+};
+
+/**
+ * Sends a message to /datum/tgui_window.
+ */
+export const sendMessage = (message = {}) => {
+ const { payload, ...rest } = message;
+ const data = {
+ // Message identifying header
+ tgui: 1,
+ window_id: window.__windowId__,
+ // Message body
+ ...rest,
+ };
+ // JSON-encode the payload
+ if (payload !== null && payload !== undefined) {
+ data.payload = JSON.stringify(payload);
+ }
+ Byond.topic(data);
+};
+
+/**
+ * Sends an action to `ui_act` on `src_object` that this tgui window
+ * is associated with.
+ */
+export const sendAct = (action, payload = {}) => {
+ // Validate that payload is an object
+ const isObject = typeof payload === 'object'
+ && payload !== null
+ && !Array.isArray(payload);
+ if (!isObject) {
+ logger.error(`Payload for act() must be an object, got this:`, payload);
+ return;
+ }
+ sendMessage({
+ type: 'act/' + action,
+ payload,
+ });
+};
+
/**
* @typedef BackendState
* @type {{
* config: {
* title: string,
* status: number,
- * screen: string,
- * style: string,
* interface: string,
- * fancy: number,
- * locked: number,
- * observer: number,
- * window: string,
- * ref: string,
+ * user: {
+ * name: string,
+ * ckey: string,
+ * observer: number,
+ * },
+ * window: {
+ * key: string,
+ * size: [number, number],
+ * fancy: boolean,
+ * locked: boolean,
+ * },
* },
* data: any,
+ * shared: any,
* visible: boolean,
* interactive: boolean,
+ * suspending: boolean,
+ * suspended: boolean,
* }}
*/
+/**
+ * Selects a backend-related slice of Redux state
+ *
+ * @return {BackendState}
+ */
+export const selectBackend = state => state.backend || {};
+
/**
* A React hook (sort of) for getting tgui state and related functions.
*
@@ -108,21 +274,16 @@ export const backendReducer = (state, action) => {
* be used in functional components.
*
* @return {BackendState & {
- * act: (action: string, params?: object) => void,
+ * act: sendAct,
* }}
*/
export const useBackend = context => {
const { store } = context;
- const state = store.getState();
- const ref = state.config.ref;
- const act = (action, params = {}) => {
- callByond('', {
- src: ref,
- action,
- ...params,
- });
+ const state = selectBackend(store.getState());
+ return {
+ ...state,
+ act: sendAct,
};
- return { ...state, act };
};
/**
@@ -140,7 +301,7 @@ export const useBackend = context => {
*/
export const useLocalState = (context, key, initialState) => {
const { store } = context;
- const state = store.getState();
+ const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
@@ -148,7 +309,11 @@ export const useLocalState = (context, key, initialState) => {
return [
sharedState,
nextState => {
- store.dispatch(backendSetSharedState(key, nextState));
+ store.dispatch(backendSetSharedState(key, (
+ typeof nextState === 'function'
+ ? nextState(sharedState)
+ : nextState
+ )));
},
];
};
@@ -169,8 +334,7 @@ export const useLocalState = (context, key, initialState) => {
*/
export const useSharedState = (context, key, initialState) => {
const { store } = context;
- const state = store.getState();
- const ref = state.config.ref;
+ const state = selectBackend(store.getState());
const sharedStates = state.shared ?? {};
const sharedState = (key in sharedStates)
? sharedStates[key]
@@ -178,11 +342,14 @@ export const useSharedState = (context, key, initialState) => {
return [
sharedState,
nextState => {
- callByond('', {
- src: ref,
- action: 'tgui:setSharedState',
+ sendMessage({
+ type: 'setSharedState',
key,
- value: JSON.stringify(nextState) || '',
+ value: JSON.stringify(
+ typeof nextState === 'function'
+ ? nextState(sharedState)
+ : nextState
+ ) || '',
});
},
];
diff --git a/tgui/packages/tgui/components/Box.js b/tgui/packages/tgui/components/Box.js
index cb86890a97..a85c692e9a 100644
--- a/tgui/packages/tgui/components/Box.js
+++ b/tgui/packages/tgui/components/Box.js
@@ -9,17 +9,22 @@ import { createVNode } from 'inferno';
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
import { CSS_COLORS } from '../constants';
-const UNIT_PX = 12;
-
/**
* Coverts our rem-like spacing unit into a CSS unit.
*/
export const unit = value => {
if (typeof value === 'string') {
+ // Transparently convert pixels into rem units
+ if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
+ return parseFloat(value) / 12 + 'rem';
+ }
return value;
}
if (typeof value === 'number') {
- return (value * UNIT_PX) + 'px';
+ if (Byond.IS_LTE_IE8) {
+ return value * 12 + 'px';
+ }
+ return value + 'rem';
}
};
@@ -28,10 +33,10 @@ export const unit = value => {
*/
export const halfUnit = value => {
if (typeof value === 'string') {
- return value;
+ return unit(value);
}
if (typeof value === 'number') {
- return (value * UNIT_PX * 0.5) + 'px';
+ return unit(value * 0.5);
}
};
@@ -90,7 +95,13 @@ const styleMapperByPropName = {
maxHeight: mapUnitPropTo('max-height', unit),
fontSize: mapUnitPropTo('font-size', unit),
fontFamily: mapRawPropTo('font-family'),
- lineHeight: mapRawPropTo('line-height'),
+ lineHeight: (style, value) => {
+ if (!isFalsy(value)) {
+ style['line-height'] = typeof value === 'number'
+ ? value
+ : unit(value);
+ }
+ },
opacity: mapRawPropTo('opacity'),
textAlign: mapRawPropTo('text-align'),
verticalAlign: mapRawPropTo('vertical-align'),
diff --git a/tgui/packages/tgui/components/Button.js b/tgui/packages/tgui/components/Button.js
index 52adf286ad..bb8b4bcbf0 100644
--- a/tgui/packages/tgui/components/Button.js
+++ b/tgui/packages/tgui/components/Button.js
@@ -6,7 +6,6 @@
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
-import { IS_IE8 } from '../byond';
import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from '../hotkeys';
import { refocusLayout } from '../layouts';
import { createLogger } from '../logging';
@@ -61,7 +60,7 @@ export const Button = props => {
className,
])}
tabIndex={!disabled && '0'}
- unselectable={IS_IE8}
+ unselectable={Byond.IS_LTE_IE8}
onclick={e => {
refocusLayout();
if (!disabled && onClick) {
@@ -87,7 +86,10 @@ export const Button = props => {
}}
{...rest}>
{icon && (
-
+
)}
{content}
{children}
diff --git a/tgui/packages/tgui/components/ByondUi.js b/tgui/packages/tgui/components/ByondUi.js
index db9c5822ba..2369cc7993 100644
--- a/tgui/packages/tgui/components/ByondUi.js
+++ b/tgui/packages/tgui/components/ByondUi.js
@@ -7,7 +7,6 @@
import { shallowDiffers } from 'common/react';
import { debounce } from 'common/timer';
import { Component, createRef } from 'inferno';
-import { callByond, IS_IE8 } from '../byond';
import { createLogger } from '../logging';
import { computeBoxProps } from './Box';
@@ -28,16 +27,12 @@ const createByondUiElement = elementId => {
render: params => {
logger.log(`rendering '${id}'`);
byondUiStack[index] = id;
- callByond('winset', {
- ...params,
- id,
- });
+ Byond.winset(id, params);
},
unmount: () => {
logger.log(`unmounting '${id}'`);
byondUiStack[index] = null;
- callByond('winset', {
- id,
+ Byond.winset(id, {
parent: '',
});
},
@@ -51,8 +46,7 @@ window.addEventListener('beforeunload', () => {
if (typeof id === 'string') {
logger.log(`unmounting '${id}' (beforeunload)`);
byondUiStack[index] = null;
- callByond('winset', {
- id,
+ Byond.winset(id, {
parent: '',
});
}
@@ -83,7 +77,7 @@ export class ByondUi extends Component {
this.byondUiElement = createByondUiElement(props.params?.id);
this.handleResize = debounce(() => {
this.forceUpdate();
- }, 500);
+ }, 100);
}
shouldComponentUpdate(nextProps) {
@@ -101,16 +95,17 @@ export class ByondUi extends Component {
componentDidMount() {
// IE8: It probably works, but fuck you anyway.
- if (IS_IE8) {
+ if (Byond.IS_LTE_IE10) {
return;
}
window.addEventListener('resize', this.handleResize);
- return this.componentDidUpdate();
+ this.componentDidUpdate();
+ this.handleResize();
}
componentDidUpdate() {
// IE8: It probably works, but fuck you anyway.
- if (IS_IE8) {
+ if (Byond.IS_LTE_IE10) {
return;
}
const {
@@ -119,6 +114,7 @@ export class ByondUi extends Component {
const box = getBoundingBox(this.containerRef.current);
logger.log('bounding box', box);
this.byondUiElement.render({
+ parent: window.__windowId__,
...params,
pos: box.pos[0] + ',' + box.pos[1],
size: box.size[0] + 'x' + box.size[1],
@@ -127,7 +123,7 @@ export class ByondUi extends Component {
componentWillUnmount() {
// IE8: It probably works, but fuck you anyway.
- if (IS_IE8) {
+ if (Byond.IS_LTE_IE10) {
return;
}
window.removeEventListener('resize', this.handleResize);
@@ -135,26 +131,16 @@ export class ByondUi extends Component {
}
render() {
- const {
- parent,
- params,
- ...rest
- } = this.props;
+ const { params, ...rest } = this.props;
const type = params?.type;
const boxProps = computeBoxProps(rest);
return (
- {type === 'button' &&
}
+ {/* Filler */}
+
);
}
}
-
-const ButtonMock = () => (
-
-);
diff --git a/tgui/packages/tgui/components/Chart.js b/tgui/packages/tgui/components/Chart.js
index 16ed82c355..77913779db 100644
--- a/tgui/packages/tgui/components/Chart.js
+++ b/tgui/packages/tgui/components/Chart.js
@@ -7,7 +7,6 @@
import { map, zipWith } from 'common/collections';
import { pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
-import { IS_IE8 } from '../byond';
import { Box } from './Box';
const normalizeData = (data, scale, rangeX, rangeY) => {
@@ -123,5 +122,5 @@ const Stub = props => null;
// IE8: No inline svg support
export const Chart = {
- Line: IS_IE8 ? Stub : LineChart,
+ Line: Byond.IS_LTE_IE8 ? Stub : LineChart,
};
diff --git a/tgui/packages/tgui/components/Flex.js b/tgui/packages/tgui/components/Flex.js
index 4ee69a1902..02d2fac314 100644
--- a/tgui/packages/tgui/components/Flex.js
+++ b/tgui/packages/tgui/components/Flex.js
@@ -5,7 +5,6 @@
*/
import { classes, pureComponentHooks } from 'common/react';
-import { IS_IE8 } from '../byond';
import { Box, unit } from './Box';
export const computeFlexProps = props => {
@@ -22,10 +21,10 @@ export const computeFlexProps = props => {
return {
className: classes([
'Flex',
- IS_IE8 && (
+ Byond.IS_LTE_IE10 && (
direction === 'column'
- ? 'Flex--ie8--column'
- : 'Flex--ie8'
+ ? 'Flex--iefix--column'
+ : 'Flex--iefix'
),
inline && 'Flex--inline',
spacing > 0 && 'Flex--spacing--' + spacing,
@@ -63,7 +62,7 @@ export const computeFlexItemProps = props => {
return {
className: classes([
'Flex__item',
- IS_IE8 && 'Flex__item--ie8',
+ Byond.IS_LTE_IE10 && 'Flex__item--iefix',
className,
]),
style: {
diff --git a/tgui/packages/tgui/components/Input.js b/tgui/packages/tgui/components/Input.js
index 25b85893ed..623fa92bb4 100644
--- a/tgui/packages/tgui/components/Input.js
+++ b/tgui/packages/tgui/components/Input.js
@@ -83,6 +83,10 @@ export class Input extends Component {
if (input) {
input.value = toInputValue(nextValue);
}
+
+ if (this.props.autoFocus) {
+ setTimeout(() => input.focus(), 1);
+ }
}
componentDidUpdate(prevProps, prevState) {
diff --git a/tgui/packages/tgui/components/Knob.js b/tgui/packages/tgui/components/Knob.js
index e72b15f195..4861e71bf3 100644
--- a/tgui/packages/tgui/components/Knob.js
+++ b/tgui/packages/tgui/components/Knob.js
@@ -6,7 +6,6 @@
import { keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
-import { IS_IE8 } from '../byond';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
import { NumberInput } from './NumberInput';
@@ -14,7 +13,7 @@ import { NumberInput } from './NumberInput';
export const Knob = props => {
// IE8: I don't want to support a yet another component on IE8.
// IE8: It also can't handle SVG.
- if (IS_IE8) {
+ if (Byond.IS_LTE_IE8) {
return (
);
diff --git a/tgui/packages/tgui/components/NumberInput.js b/tgui/packages/tgui/components/NumberInput.js
index 806c81d542..cba6f5025e 100644
--- a/tgui/packages/tgui/components/NumberInput.js
+++ b/tgui/packages/tgui/components/NumberInput.js
@@ -7,7 +7,6 @@
import { clamp } from 'common/math';
import { classes, pureComponentHooks } from 'common/react';
import { Component, createRef } from 'inferno';
-import { IS_IE8 } from '../byond';
import { AnimatedNumber } from './AnimatedNumber';
import { Box } from './Box';
@@ -168,7 +167,7 @@ export class NumberInput extends Component {
const renderContentElement = value => (
+ unselectable={Byond.IS_LTE_IE8}>
{value + (unit ? ' ' + unit : '')}
);
diff --git a/tgui/packages/tgui/components/Section.js b/tgui/packages/tgui/components/Section.js
index 2f9fa239b1..af59bc4022 100644
--- a/tgui/packages/tgui/components/Section.js
+++ b/tgui/packages/tgui/components/Section.js
@@ -5,7 +5,7 @@
*/
import { classes, isFalsy, pureComponentHooks } from 'common/react';
-import { Box } from './Box';
+import { computeBoxClassName, computeBoxProps } from './Box';
export const Section = props => {
const {
@@ -13,20 +13,22 @@ export const Section = props => {
title,
level = 1,
buttons,
- content,
+ fill,
children,
...rest
} = props;
const hasTitle = !isFalsy(title) || !isFalsy(buttons);
- const hasContent = !isFalsy(content) || !isFalsy(children);
+ const hasContent = !isFalsy(children);
return (
-
+ {...computeBoxProps(rest)}>
{hasTitle && (
@@ -39,11 +41,10 @@ export const Section = props => {
)}
{hasContent && (
- {content}
{children}
)}
-
+
);
};
diff --git a/tgui/packages/tgui/components/Slider.js b/tgui/packages/tgui/components/Slider.js
index 7ae74684fb..005e6c1f8f 100644
--- a/tgui/packages/tgui/components/Slider.js
+++ b/tgui/packages/tgui/components/Slider.js
@@ -6,14 +6,13 @@
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
import { classes } from 'common/react';
-import { IS_IE8 } from '../byond';
import { computeBoxClassName, computeBoxProps } from './Box';
import { DraggableControl } from './DraggableControl';
import { NumberInput } from './NumberInput';
export const Slider = props => {
// IE8: I don't want to support a yet another component on IE8.
- if (IS_IE8) {
+ if (Byond.IS_LTE_IE8) {
return (
);
diff --git a/tgui/packages/tgui/components/index.js b/tgui/packages/tgui/components/index.js
index e7631116f7..5580cc38fb 100644
--- a/tgui/packages/tgui/components/index.js
+++ b/tgui/packages/tgui/components/index.js
@@ -14,6 +14,7 @@ export { Collapsible } from './Collapsible';
export { ColorBox } from './ColorBox';
export { Dimmer } from './Dimmer';
export { Divider } from './Divider';
+export { DraggableControl } from './DraggableControl';
export { Dropdown } from './Dropdown';
export { Flex } from './Flex';
export { Grid } from './Grid';
@@ -29,7 +30,7 @@ export { ProgressBar } from './ProgressBar';
export { Section } from './Section';
export { Slider } from './Slider';
export { Table } from './Table';
+export { TextArea } from './TextArea';
export { Tabs } from './Tabs';
export { Tooltip } from './Tooltip';
export { TimeDisplay } from './TimeDisplay';
-// export { TextArea } from './TextArea'; PAPER
diff --git a/tgui/packages/tgui/debug/KitchenSink.js b/tgui/packages/tgui/debug/KitchenSink.js
new file mode 100644
index 0000000000..050adcf193
--- /dev/null
+++ b/tgui/packages/tgui/debug/KitchenSink.js
@@ -0,0 +1,608 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { Fragment } from 'inferno';
+import { useBackend, useLocalState } from '../backend';
+import { BlockQuote, Box, Button, ByondUi, Collapsible, DraggableControl, Flex, Icon, Input, Knob, LabeledList, NoticeBox, NumberInput, ProgressBar, Section, Slider, Tabs, Tooltip } from '../components';
+import { formatSiUnit } from '../format';
+import { Window } from '../layouts';
+import { createLogger } from '../logging';
+
+const logger = createLogger('KitchenSink');
+
+const COLORS_SPECTRUM = [
+ 'red',
+ 'orange',
+ 'yellow',
+ 'olive',
+ 'green',
+ 'teal',
+ 'blue',
+ 'violet',
+ 'purple',
+ 'pink',
+ 'brown',
+ 'grey',
+];
+
+const COLORS_STATES = [
+ 'good',
+ 'average',
+ 'bad',
+ 'black',
+ 'white',
+];
+
+const PAGES = [
+ {
+ title: 'Button',
+ component: () => KitchenSinkButton,
+ },
+ {
+ title: 'Box',
+ component: () => KitchenSinkBox,
+ },
+ {
+ title: 'Flex & Sections',
+ component: () => KitchenSinkFlexAndSections,
+ },
+ {
+ title: 'ProgressBar',
+ component: () => KitchenSinkProgressBar,
+ },
+ {
+ title: 'Tabs',
+ component: () => KitchenSinkTabs,
+ },
+ {
+ title: 'Tooltip',
+ component: () => KitchenSinkTooltip,
+ },
+ {
+ title: 'Input / Control',
+ component: () => KitchenSinkInput,
+ },
+ {
+ title: 'Collapsible',
+ component: () => KitchenSinkCollapsible,
+ },
+ {
+ title: 'BlockQuote',
+ component: () => KitchenSinkBlockQuote,
+ },
+ {
+ title: 'ByondUi',
+ component: () => KitchenSinkByondUi,
+ },
+ {
+ title: 'Themes',
+ component: () => KitchenSinkThemes,
+ },
+ {
+ title: 'Storage',
+ component: () => KitchenSinkStorage,
+ },
+];
+
+export const KitchenSink = (props, context) => {
+ const [theme] = useLocalState(context, 'kitchenSinkTheme');
+ const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0);
+ const PageComponent = PAGES[pageIndex].component();
+ return (
+
+
+
+
+ {PAGES.map((page, i) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const KitchenSinkButton = props => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {COLORS_STATES.map(color => (
+
+ ))}
+
+ {COLORS_SPECTRUM.map(color => (
+
+ ))}
+
+ {COLORS_SPECTRUM.map(color => (
+
+ {color}
+
+ ))}
+
+
+ );
+};
+
+const KitchenSinkBox = props => {
+ return (
+
+
+ bold
+
+
+ italic
+
+
+ opacity 0.5
+
+
+ opacity 0.25
+
+
+ m: 2
+
+
+ left
+
+
+ center
+
+
+ right
+
+
+ );
+};
+
+const KitchenSinkFlexAndSections = (props, context) => {
+ const [grow, setGrow] = useLocalState(
+ context, 'fs_grow', 1);
+ const [direction, setDirection] = useLocalState(
+ context, 'fs_direction', 'column');
+ const [fill, setFill] = useLocalState(
+ context, 'fs_fill', true);
+ const [hasTitle, setHasTitle] = useLocalState(
+ context, 'fs_title', true);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const KitchenSinkProgressBar = (props, context) => {
+ const [
+ progress,
+ setProgress,
+ ] = useLocalState(context, 'progress', 0.5);
+ return (
+
+
+ Value: {Number(progress).toFixed(1)}
+
+
+
+
+ );
+};
+
+const KitchenSinkTabs = (props, context) => {
+ const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
+ const [vertical, setVertical] = useLocalState(context, 'tabVert');
+ const [altSelection, setAltSelection] = useLocalState(context, 'tabAlt');
+ const TAB_RANGE = [1, 2, 3, 4, 5];
+ return (
+
+
+ setVertical(!vertical)} />
+ setAltSelection(!altSelection)} />
+
+
+ {TAB_RANGE.map((number, i) => (
+ setTabIndex(i)}>
+ Tab #{number}
+
+ ))}
+
+
+ );
+};
+
+const KitchenSinkTooltip = props => {
+ const positions = [
+ 'top',
+ 'left',
+ 'right',
+ 'bottom',
+ 'bottom-left',
+ 'bottom-right',
+ ];
+ return (
+
+
+
+ Box (hover me).
+
+
+
+
+
+ {positions.map(position => (
+
+ ))}
+
+
+ );
+};
+
+const KitchenSinkInput = (props, context) => {
+ const [
+ number,
+ setNumber,
+ ] = useLocalState(context, 'number', 0);
+ const [
+ text,
+ setText,
+ ] = useLocalState(context, 'text', "Sample text");
+ return (
+
+ );
+};
+
+const KitchenSinkCollapsible = props => {
+ return (
+
+ );
+};
+
+const BoxWithSampleText = props => {
+ return (
+
+
+ Jackdaws love my big sphinx of quartz.
+
+
+ The wide electrification of the southern
+ provinces will give a powerful impetus to the
+ growth of agriculture.
+
+
+ );
+};
+
+const KitchenSinkBlockQuote = props => {
+ return (
+
+ );
+};
+
+const KitchenSinkByondUi = (props, context) => {
+ const { config } = useBackend(context);
+ const [code, setCode] = useLocalState(context,
+ 'byondUiEvalCode',
+ `Byond.winset('${window.__windowId__}', {\n 'is-visible': true,\n})`);
+ return (
+
+
+ setImmediate(() => {
+ try {
+ const result = new Function('return (' + code + ')')();
+ if (result && result.then) {
+ logger.log('Promise');
+ result.then(logger.log);
+ }
+ else {
+ logger.log(result);
+ }
+ }
+ catch (err) {
+ logger.log(err);
+ }
+ })}>
+ Evaluate
+
+ )}>
+ setCode(e.target.value)}>
+ {code}
+
+
+
+ );
+};
+
+const KitchenSinkThemes = (props, context) => {
+ const [theme, setTheme] = useLocalState(context, 'kitchenSinkTheme');
+ return (
+
+ );
+};
+
+const KitchenSinkStorage = (props, context) => {
+ if (!window.localStorage) {
+ return (
+
+ Local storage is not available.
+
+ );
+ }
+ return (
+ {
+ localStorage.clear();
+ }}>
+ Clear
+
+ )}>
+
+
+ {localStorage.length}
+
+
+ {formatSiUnit(localStorage.remainingSpace, 0, 'B')}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/debug/index.js b/tgui/packages/tgui/debug/index.js
new file mode 100644
index 0000000000..83fc136548
--- /dev/null
+++ b/tgui/packages/tgui/debug/index.js
@@ -0,0 +1,49 @@
+/**
+ * @file
+ * @copyright 2020 Aleksej Komarov
+ * @license MIT
+ */
+
+import { subscribeToHotKey } from '../hotkeys';
+
+export const toggleKitchenSink = () => ({
+ type: 'debug/toggleKitchenSink',
+});
+
+export const toggleDebugLayout = () => ({
+ type: 'debug/toggleDebugLayout',
+});
+
+subscribeToHotKey('F11', () => toggleDebugLayout());
+subscribeToHotKey('F12', () => toggleKitchenSink());
+subscribeToHotKey('Ctrl+Alt+[8]', () => {
+ // NOTE: We need to call this in a timeout, because we need a clean
+ // stack in order for this to be a fatal error.
+ setTimeout(() => {
+ throw new Error(
+ 'OOPSIE WOOPSIE!! UwU We made a fucky wucky!! A wittle'
+ + ' fucko boingo! The code monkeys at our headquarters are'
+ + ' working VEWY HAWD to fix this!');
+ });
+});
+
+export const selectDebug = state => state.debug;
+
+export const useDebug = context => selectDebug(context.store.getState());
+
+export const debugReducer = (state = {}, action) => {
+ const { type, payload } = action;
+ if (type === 'debug/toggleKitchenSink') {
+ return {
+ ...state,
+ kitchenSink: !state.kitchenSink,
+ };
+ }
+ if (type === 'debug/toggleDebugLayout') {
+ return {
+ ...state,
+ debugLayout: !state.debugLayout,
+ };
+ }
+ return state;
+};
diff --git a/tgui/packages/tgui/drag.js b/tgui/packages/tgui/drag.js
index 1b735cb818..99db716410 100644
--- a/tgui/packages/tgui/drag.js
+++ b/tgui/packages/tgui/drag.js
@@ -4,79 +4,171 @@
* @license MIT
*/
-import { vecAdd, vecInverse, vecMultiply } from 'common/vector';
-import { winget, winset } from './byond';
+import { storage } from 'common/storage';
+import { vecAdd, vecInverse, vecMultiply, vecScale } from 'common/vector';
import { createLogger } from './logging';
const logger = createLogger('drag');
-let ref;
+let windowKey = window.__windowId__;
let dragging = false;
let resizing = false;
let screenOffset = [0, 0];
+let screenOffsetPromise;
let dragPointOffset;
let resizeMatrix;
let initialSize;
let size;
-const getWindowPosition = ref => {
- return winget(ref, 'pos').then(pos => [pos.x, pos.y]);
+export const setWindowKey = key => {
+ windowKey = key;
};
-const setWindowPosition = (ref, vec) => {
- return winset(ref, 'pos', vec[0] + ',' + vec[1]);
+export const getWindowPosition = () => [
+ window.screenLeft,
+ window.screenTop,
+];
+
+export const getWindowSize = () => [
+ window.innerWidth,
+ window.innerHeight,
+];
+
+export const setWindowPosition = vec => {
+ const byondPos = vecAdd(vec, screenOffset);
+ return Byond.winset(window.__windowId__, {
+ pos: byondPos[0] + ',' + byondPos[1],
+ });
};
-const setWindowSize = (ref, vec) => {
- return winset(ref, 'size', vec[0] + ',' + vec[1]);
+export const setWindowSize = vec => {
+ return Byond.winset(window.__windowId__, {
+ size: vec[0] + 'x' + vec[1],
+ });
};
-export const setupDrag = async state => {
- logger.log('setting up');
- ref = state.config.window;
- // Calculate offset caused by windows taskbar
- const realPosition = await getWindowPosition(ref);
- screenOffset = [
- realPosition[0] - window.screenLeft,
- realPosition[1] - window.screenTop,
- ];
- // Constraint window position
- const [relocated, safePosition] = constraintPosition(realPosition);
- if (relocated) {
- setWindowPosition(ref, safePosition);
+export const getScreenPosition = () => [
+ 0 - screenOffset[0],
+ 0 - screenOffset[1],
+];
+
+export const getScreenSize = () => [
+ window.screen.availWidth,
+ window.screen.availHeight,
+];
+
+/**
+ * Moves an item to the top of the recents array, and keeps its length
+ * limited to the number in `limit` argument.
+ *
+ * Uses a strict equality check for comparisons.
+ *
+ * Returns new recents and an item which was trimmed.
+ */
+const touchRecents = (recents, touchedItem, limit = 50) => {
+ const nextRecents = [touchedItem];
+ let trimmedItem;
+ for (let i = 0; i < recents.length; i++) {
+ const item = recents[i];
+ if (item === touchedItem) {
+ continue;
+ }
+ if (nextRecents.length < limit) {
+ nextRecents.push(item);
+ }
+ else {
+ trimmedItem = item;
+ }
}
- logger.debug('current state', { ref, screenOffset });
+ return [nextRecents, trimmedItem];
+};
+
+export const storeWindowGeometry = windowKey => {
+ logger.log('storing geometry');
+ const geometry = {
+ pos: getWindowPosition(),
+ size: getWindowSize(),
+ };
+ storage.set(windowKey, geometry);
+ // Update the list of stored geometries
+ const [geometries, trimmedKey] = touchRecents(
+ storage.get('geometries') || [],
+ windowKey);
+ if (trimmedKey) {
+ storage.remove(trimmedKey);
+ }
+ storage.set('geometries', geometries);
+};
+
+export const recallWindowGeometry = async (windowKey, options = {}) => {
+ // Only recall geometry in fancy mode
+ const geometry = options.fancy && storage.get(windowKey);
+ if (geometry) {
+ logger.log('recalled geometry:', geometry);
+ }
+ let pos = geometry?.pos || options.pos;
+ const size = options.size;
+ // Set window size
+ if (size) {
+ setWindowSize(size);
+ }
+ // Set window position
+ if (pos) {
+ await screenOffsetPromise;
+ // Constraint window position if monitor lock was set in preferences.
+ if (size && options.locked) {
+ pos = constraintPosition(pos, size)[1];
+ }
+ setWindowPosition(pos);
+ }
+ // Set window position at the center of the screen.
+ else if (size) {
+ await screenOffsetPromise;
+ const areaAvailable = [
+ window.screen.availWidth - Math.abs(screenOffset[0]),
+ window.screen.availHeight - Math.abs(screenOffset[1]),
+ ];
+ const pos = vecAdd(
+ vecScale(areaAvailable, 0.5),
+ vecScale(size, -0.5),
+ vecScale(screenOffset, -1.0));
+ setWindowPosition(pos);
+ }
+};
+
+export const setupDrag = async () => {
+ // Calculate screen offset caused by the windows taskbar
+ screenOffsetPromise = Byond.winget(window.__windowId__, 'pos')
+ .then(pos => [
+ pos.x - window.screenLeft,
+ pos.y - window.screenTop,
+ ]);
+ screenOffset = await screenOffsetPromise;
+ logger.debug('screen offset', screenOffset);
};
/**
* Constraints window position to safe screen area, accounting for safe
* margins which could be a system taskbar.
*/
-const constraintPosition = position => {
- let x = position[0];
- let y = position[1];
+const constraintPosition = (pos, size) => {
+ const screenPos = getScreenPosition();
+ const screenSize = getScreenSize();
+ const nextPos = [pos[0], pos[1]];
let relocated = false;
- // Left
- if (x < 0) {
- x = 0;
- relocated = true;
+ for (let i = 0; i < 2; i++) {
+ const leftBoundary = screenPos[i];
+ const rightBoundary = screenPos[i] + screenSize[i];
+ if (pos[i] < leftBoundary) {
+ nextPos[i] = leftBoundary;
+ relocated = true;
+ }
+ else if (pos[i] + size[i] > rightBoundary) {
+ nextPos[i] = rightBoundary - size[i];
+ relocated = true;
+ }
}
- // Right
- else if (x + window.innerWidth > window.screen.availWidth) {
- x = window.screen.availWidth - window.innerWidth;
- relocated = true;
- }
- // Top
- if (y < 0) {
- y = 0;
- relocated = true;
- }
- // Bottom
- else if (y + window.innerHeight > window.screen.availHeight) {
- y = window.screen.availHeight - window.innerHeight;
- relocated = true;
- }
- return [relocated, [x, y]];
+ return [relocated, nextPos];
};
export const dragStartHandler = event => {
@@ -97,6 +189,7 @@ const dragEndHandler = event => {
document.removeEventListener('mousemove', dragMoveHandler);
document.removeEventListener('mouseup', dragEndHandler);
dragging = false;
+ storeWindowGeometry(windowKey);
};
const dragMoveHandler = event => {
@@ -104,9 +197,8 @@ const dragMoveHandler = event => {
return;
}
event.preventDefault();
- setWindowPosition(ref, vecAdd(
+ setWindowPosition(vecAdd(
[event.screenX, event.screenY],
- screenOffset,
dragPointOffset));
};
@@ -133,6 +225,7 @@ const resizeEndHandler = event => {
document.removeEventListener('mousemove', resizeMoveHandler);
document.removeEventListener('mouseup', resizeEndHandler);
resizing = false;
+ storeWindowGeometry(windowKey);
};
const resizeMoveHandler = event => {
@@ -146,7 +239,7 @@ const resizeMoveHandler = event => {
dragPointOffset,
[1, 1])));
// Sane window size values
- size[0] = Math.max(size[0], 250);
- size[1] = Math.max(size[1], 120);
- setWindowSize(ref, size);
+ size[0] = Math.max(size[0], 150);
+ size[1] = Math.max(size[1], 50);
+ setWindowSize(size);
};
diff --git a/tgui/packages/tgui/format.js b/tgui/packages/tgui/format.js
index 4a72658723..1f8420860e 100644
--- a/tgui/packages/tgui/format.js
+++ b/tgui/packages/tgui/format.js
@@ -39,6 +39,9 @@ export const formatSiUnit = (
minBase1000 = -SI_BASE_INDEX,
unit = ''
) => {
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
+ return value;
+ }
const realBase10 = Math.floor(Math.log10(value));
const base10 = Math.floor(Math.max(minBase1000 * 3, realBase10));
const realBase1000 = Math.floor(realBase10 / 3);
diff --git a/tgui/packages/tgui/global.d.ts b/tgui/packages/tgui/global.d.ts
new file mode 100644
index 0000000000..ab8870f15c
--- /dev/null
+++ b/tgui/packages/tgui/global.d.ts
@@ -0,0 +1,95 @@
+interface ByondType {
+ /**
+ * True if javascript is running in BYOND.
+ */
+ IS_BYOND: boolean;
+
+ /**
+ * True if browser is IE8 or lower.
+ */
+ IS_LTE_IE8: boolean;
+
+ /**
+ * True if browser is IE9 or lower.
+ */
+ IS_LTE_IE9: boolean;
+
+ /**
+ * True if browser is IE10 or lower.
+ */
+ IS_LTE_IE10: boolean;
+
+ /**
+ * True if browser is IE11 or lower.
+ */
+ IS_LTE_IE11: boolean;
+
+ /**
+ * Makes a BYOND call.
+ *
+ * If path is empty, this will trigger a Topic call.
+ * You can reference a specific object by setting the "src" parameter.
+ *
+ * See: https://secure.byond.com/docs/ref/skinparams.html
+ */
+ call(path: string, params: object): void;
+
+ /**
+ * Makes an asynchronous BYOND call. Returns a promise.
+ */
+ callAsync(path: string, params: object): Promise;
+
+ /**
+ * Makes a Topic call.
+ *
+ * You can reference a specific object by setting the "src" parameter.
+ */
+ topic(params: object): void;
+
+ /**
+ * Runs a command or a verb.
+ */
+ command(command: string): void;
+
+ /**
+ * Retrieves all properties of the BYOND skin element.
+ *
+ * Returns a promise with a key-value object containing all properties.
+ */
+ winget(id: string): Promise