mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 02:34:42 +00:00
Keyboard and mouse controls will now work if you use the kbMouseSupport parameter in the config for Libretro cores. Be aware that capturing mouse and keyboard controls properly is only possible in fullscreen mode. Note: In the case of DOSBox, a virtual filesystem handler is not yet implemented, thus each game state will be shared between all rooms (DOS game instances) of CloudRetro.
277 lines
6.9 KiB
JavaScript
277 lines
6.9 KiB
JavaScript
/**
|
|
* App UI elements module.
|
|
*/
|
|
|
|
const _create = (name = 'div', modFn) => {
|
|
const el = document.createElement(name);
|
|
if (modFn) {
|
|
modFn(el);
|
|
}
|
|
return el;
|
|
}
|
|
|
|
const _option = (text = '', selected = false, label) => {
|
|
const el = _create('option');
|
|
if (label) {
|
|
el.textContent = label;
|
|
el.value = text;
|
|
} else {
|
|
el.textContent = text;
|
|
}
|
|
if (selected) el.selected = true;
|
|
|
|
return el;
|
|
}
|
|
|
|
const select = (key = '', callback = () => ({}), values = {values: [], labels: []}, current = '') => {
|
|
const el = _create();
|
|
const select = _create('select');
|
|
select.onchange = event => {
|
|
callback(key, event.target.value);
|
|
};
|
|
el.append(select);
|
|
|
|
select.append(_option(0, current === '', 'none'));
|
|
values.values.forEach((value, index) => {
|
|
select.append(_option(value, current == value, values.labels?.[index]));
|
|
});
|
|
|
|
return el;
|
|
}
|
|
|
|
const checkbox = (id, cb = () => ({}), checked = false, label = '', cc = '') => {
|
|
const el = _create();
|
|
cc !== '' && el.classList.add(cc);
|
|
|
|
let parent = el;
|
|
|
|
if (label) {
|
|
const _label = _create('label', (el) => {
|
|
el.setAttribute('htmlFor', id);
|
|
})
|
|
_label.innerText = label;
|
|
el.append(_label)
|
|
parent = _label;
|
|
}
|
|
|
|
const input = _create('input', (el) => {
|
|
el.setAttribute('id', id);
|
|
el.setAttribute('name', id);
|
|
el.setAttribute('type', 'checkbox');
|
|
el.onclick = ((e) => {
|
|
checked = e.target.checked
|
|
cb(id, checked)
|
|
})
|
|
checked && el.setAttribute('checked', '');
|
|
});
|
|
parent.prepend(input);
|
|
|
|
return el;
|
|
}
|
|
|
|
const panel = (root, title = '', cc = '', content, buttons = [], onToggle) => {
|
|
const state = {
|
|
shown: false,
|
|
loading: false,
|
|
title: title,
|
|
}
|
|
|
|
const tHandlers = [];
|
|
onToggle && tHandlers.push(onToggle);
|
|
|
|
const _root = root || _create('div');
|
|
_root.classList.add('panel');
|
|
gui.hide(_root);
|
|
|
|
const header = _create('div', (el) => el.classList.add('panel__header'));
|
|
const _content = _create('div', (el) => {
|
|
if (cc) {
|
|
el.classList.add(cc);
|
|
}
|
|
el.classList.add('panel__content')
|
|
});
|
|
|
|
const _title = _create('span', (el) => {
|
|
el.classList.add('panel__header__title');
|
|
el.innerText = title;
|
|
});
|
|
header.append(_title);
|
|
|
|
header.append(_create('div', (el) => {
|
|
el.classList.add('panel__header__controls');
|
|
|
|
buttons.forEach((b => el.append(_create('span', (el) => {
|
|
if (Object.keys(b).length === 0) {
|
|
el.classList.add('panel__button_separator');
|
|
return
|
|
}
|
|
el.classList.add('panel__button');
|
|
if (b.cl) b.cl.forEach(class_ => el.classList.add(class_));
|
|
if (b.title) el.title = b.title;
|
|
el.innerText = b.caption;
|
|
el.addEventListener('click', b.handler)
|
|
}))))
|
|
|
|
el.append(_create('span', (el) => {
|
|
el.classList.add('panel__button');
|
|
el.innerText = 'X';
|
|
el.title = 'Close';
|
|
el.addEventListener('click', () => toggle(false))
|
|
}))
|
|
}))
|
|
|
|
root.append(header, _content);
|
|
if (content) {
|
|
_content.append(content);
|
|
}
|
|
|
|
const setContent = (content) => _content.replaceChildren(content)
|
|
|
|
const setLoad = (load = true) => {
|
|
state.loading = load;
|
|
_title.innerText = state.loading ? `${state.title}...` : state.title;
|
|
}
|
|
|
|
const toggle = (() => {
|
|
let br = window.getComputedStyle(_root.parentElement).borderRadius;
|
|
return (force) => {
|
|
state.shown = force !== undefined ? force : !state.shown;
|
|
// hack for not transparent jpeg corners :_;
|
|
_root.parentElement.style.borderRadius = state.shown ? '0px' : br;
|
|
tHandlers.forEach(h => h?.(state.shown, _root));
|
|
state.shown ? gui.show(_root) : gui.hide(_root)
|
|
}
|
|
})()
|
|
|
|
return {
|
|
contentEl: _content,
|
|
isHidden: () => !state.shown,
|
|
onToggle: (fn) => tHandlers.push(fn),
|
|
setContent,
|
|
setLoad,
|
|
toggle,
|
|
}
|
|
}
|
|
|
|
const _bind = (cb = () => ({}), name = '', oldValue) => {
|
|
const el = _create('button');
|
|
el.onclick = () => cb(name, oldValue);
|
|
el.textContent = name;
|
|
return el;
|
|
}
|
|
|
|
const binding = (key = '', value = '', cb = () => ({})) => {
|
|
const el = _create();
|
|
el.setAttribute('class', 'binding-element');
|
|
|
|
const k = _bind(cb, key, value);
|
|
|
|
el.append(k);
|
|
|
|
const v = _create();
|
|
v.textContent = value;
|
|
el.append(v);
|
|
|
|
return el;
|
|
}
|
|
|
|
const show = (...els) => {
|
|
els.forEach(el => el.classList.remove('hidden'))
|
|
}
|
|
|
|
const inputN = (key = '', cb = () => ({}), current = 0) => {
|
|
const el = _create();
|
|
const input = _create('input');
|
|
input.type = 'number';
|
|
input.value = current;
|
|
input.onchange = event => cb(key, event.target.value);
|
|
el.append(input);
|
|
return el;
|
|
}
|
|
|
|
const hide = (el) => {
|
|
el.classList.add('hidden');
|
|
}
|
|
|
|
const toggle = (el, what) => {
|
|
if (what === undefined) {
|
|
el.classList.toggle('hidden')
|
|
return
|
|
}
|
|
what ? show(el) : hide(el)
|
|
}
|
|
|
|
const multiToggle = (elements = [], options = {list: []}) => {
|
|
if (!options.list.length || !elements.length) return
|
|
|
|
let i = 0
|
|
|
|
const setText = () => elements.forEach(el => el.innerText = options.list[i].caption)
|
|
|
|
const handleClick = () => {
|
|
options.list[i].cb()
|
|
i = (i + 1) % options.list.length
|
|
setText()
|
|
}
|
|
|
|
setText()
|
|
elements.forEach(el => el.addEventListener('click', handleClick))
|
|
}
|
|
|
|
const fadeIn = async (el, speed = .1) => {
|
|
el.style.opacity = '0';
|
|
el.style.display = 'block';
|
|
return new Promise((done) => (function fade() {
|
|
let val = parseFloat(el.style.opacity);
|
|
const proceed = ((val += speed) <= 1);
|
|
if (proceed) {
|
|
el.style.opacity = '' + val;
|
|
requestAnimationFrame(fade);
|
|
} else {
|
|
done();
|
|
}
|
|
})()
|
|
);
|
|
}
|
|
|
|
const fadeOut = async (el, speed = .1) => {
|
|
el.style.opacity = '1';
|
|
return new Promise((done) => (function fade() {
|
|
if ((el.style.opacity -= speed) < 0) {
|
|
el.style.display = "none";
|
|
done();
|
|
} else {
|
|
requestAnimationFrame(fade);
|
|
}
|
|
})()
|
|
)
|
|
}
|
|
|
|
const fragment = () => document.createDocumentFragment();
|
|
|
|
const sleep = async (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
|
|
const fadeInOut = async (el, wait = 1000, speed = .1) => {
|
|
await fadeIn(el, speed)
|
|
await sleep(wait);
|
|
await fadeOut(el, speed)
|
|
}
|
|
|
|
export const gui = {
|
|
anim: {
|
|
fadeIn,
|
|
fadeOut,
|
|
fadeInOut,
|
|
},
|
|
binding,
|
|
checkbox,
|
|
create: _create,
|
|
fragment,
|
|
hide,
|
|
inputN,
|
|
multiToggle,
|
|
panel,
|
|
select,
|
|
show,
|
|
toggle,
|
|
}
|