diff --git a/src/node/StreamWindow.js b/src/node/StreamWindow.js index 57c08e1..3455fd0 100644 --- a/src/node/StreamWindow.js +++ b/src/node/StreamWindow.js @@ -5,7 +5,7 @@ import { BrowserView, BrowserWindow, ipcMain } from 'electron' import { interpret } from 'xstate' import viewStateMachine from './viewStateMachine' -import { boxesFromViewContentMap } from './geometry' +import { boxesFromViewContentMap } from '../geometry' export default class StreamWindow extends EventEmitter { constructor(config) { diff --git a/src/node/geometry.js b/src/node/geometry.js deleted file mode 100644 index b4081b6..0000000 --- a/src/node/geometry.js +++ /dev/null @@ -1,61 +0,0 @@ -import isEqual from 'lodash/isEqual' - -export function boxesFromViewContentMap(width, height, viewContentMap) { - const boxes = [] - const visited = new Set() - - function isPosContent(x, y, content) { - const checkIdx = width * y + x - return ( - !visited.has(checkIdx) && isEqual(viewContentMap.get(checkIdx), content) - ) - } - - function findLargestBox(x, y) { - const idx = width * y + x - const spaces = [idx] - const content = viewContentMap.get(idx) - - let maxY - for (maxY = y + 1; maxY < height; maxY++) { - if (!isPosContent(x, maxY, content)) { - break - } - spaces.push(width * maxY + x) - } - - let cx = x - let cy = y - scan: for (cx = x + 1; cx < width; cx++) { - for (cy = y; cy < maxY; cy++) { - if (!isPosContent(cx, cy, content)) { - break scan - } - } - for (let cy = y; cy < maxY; cy++) { - spaces.push(width * cy + cx) - } - } - const w = cx - x - const h = maxY - y - spaces.sort() - return { content, x, y, w, h, spaces } - } - - for (let y = 0; y < width; y++) { - for (let x = 0; x < height; x++) { - const idx = width * y + x - if (visited.has(idx) || viewContentMap.get(idx) === undefined) { - continue - } - - const box = findLargestBox(x, y) - boxes.push(box) - for (const boxIdx of box.spaces) { - visited.add(boxIdx) - } - } - } - - return boxes -} diff --git a/src/node/geometry.test.js b/src/node/geometry.test.js deleted file mode 100644 index 360651b..0000000 --- a/src/node/geometry.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { boxesFromViewContentMap } from './geometry' - -function example([text]) { - return text - .replace(/\s/g, '') - .split('') - .map((c) => (c === '.' ? undefined : { url: c })) -} - -const box1 = example` - ab - ab -` - -const box2 = example` - aa - bb -` - -const box3 = example` - aac - aaa - dae -` - -const box4 = example` - ... - .aa - .aa -` - -const box5 = example` - ..a - ..a - .aa -` - -describe.each([ - [ - 2, - 2, - box1, - [ - { content: { url: 'a' }, x: 0, y: 0, w: 1, h: 2, spaces: [0, 2] }, - { content: { url: 'b' }, x: 1, y: 0, w: 1, h: 2, spaces: [1, 3] }, - ], - ], - [ - 2, - 2, - box2, - [ - { content: { url: 'a' }, x: 0, y: 0, w: 2, h: 1, spaces: [0, 1] }, - { content: { url: 'b' }, x: 0, y: 1, w: 2, h: 1, spaces: [2, 3] }, - ], - ], - [ - 3, - 3, - box3, - [ - { content: { url: 'a' }, x: 0, y: 0, w: 2, h: 2, spaces: [0, 1, 3, 4] }, - { content: { url: 'c' }, x: 2, y: 0, w: 1, h: 1, spaces: [2] }, - { content: { url: 'a' }, x: 2, y: 1, w: 1, h: 1, spaces: [5] }, - { content: { url: 'd' }, x: 0, y: 2, w: 1, h: 1, spaces: [6] }, - { content: { url: 'a' }, x: 1, y: 2, w: 1, h: 1, spaces: [7] }, - { content: { url: 'e' }, x: 2, y: 2, w: 1, h: 1, spaces: [8] }, - ], - ], - [ - 3, - 3, - box4, - [{ content: { url: 'a' }, x: 1, y: 1, w: 2, h: 2, spaces: [4, 5, 7, 8] }], - ], - [ - 3, - 3, - box5, - [ - { content: { url: 'a' }, x: 2, y: 0, w: 1, h: 3, spaces: [2, 5, 8] }, - { content: { url: 'a' }, x: 1, y: 2, w: 1, h: 1, spaces: [7] }, - ], - ], -])('boxesFromViewContentMap(%i, %i, %j)', (width, height, data, expected) => { - test(`returns expected ${expected.length} boxes`, () => { - const stateURLMap = new Map(data.map((v, idx) => [idx, v])) - const result = boxesFromViewContentMap(width, height, stateURLMap) - expect(result).toStrictEqual(expected) - }) -}) diff --git a/src/web/control.js b/src/web/control.js index d4807ce..c53a5c6 100644 --- a/src/web/control.js +++ b/src/web/control.js @@ -11,6 +11,7 @@ import styled, { css } from 'styled-components' import { useHotkeys } from 'react-hotkeys-hook' import '../index.css' +import { idxInBox } from '../geometry' import SoundIcon from '../static/volume-up-solid.svg' import NoVideoIcon from '../static/video-slash-solid.svg' import ReloadIcon from '../static/redo-alt-solid.svg' @@ -185,9 +186,33 @@ function App({ wsEndpoint }) { stateIdxMap, delayState, } = useStreamwallConnection(wsEndpoint) - const { gridCount } = config + const [dragStart, setDragStart] = useState() + const handleDragStart = useCallback((idx, ev) => { + setDragStart(idx) + ev.preventDefault() + }, []) + const [dragEnd, setDragEnd] = useState() + useEffect(() => { + function endDrag() { + if (dragStart !== undefined) { + stateDoc.transact(() => { + const viewsState = stateDoc.getMap('views') + const streamId = viewsState.get(String(dragStart)).get('streamId') + for (let idx = 0; idx < gridCount ** 2; idx++) { + if (idxInBox(gridCount, dragStart, dragEnd, idx)) { + viewsState.get(String(idx)).set('streamId', streamId) + } + } + }) + setDragStart() + } + } + window.addEventListener('mouseup', endDrag) + return () => window.removeEventListener('mouseup', endDrag) + }, [stateDoc, dragStart, dragEnd]) + const handleSetView = useCallback( (idx, streamId) => { const stream = streams.find((d) => d._id === streamId) @@ -344,6 +369,9 @@ function App({ wsEndpoint }) { const { isListening = false, isBlurred = false, state } = stateIdxMap.get(idx) || {} const { streamId } = sharedState.views?.[idx] || '' + const isDragHighlighted = + dragStart !== undefined && + idxInBox(gridCount, dragStart, dragEnd, idx) return ( { - ev.target.select() - }) + const handleMouseDown = useCallback( + (ev) => { + ev.target.select() + onMouseDown(idx, ev) + }, + [onMouseDown], + ) + const handleMouseEnter = useCallback(() => onMouseEnter(idx), [onMouseEnter]) return ( {isDisplaying && ( @@ -552,9 +591,11 @@ function GridInput({ name={idx} value={editingValue || spaceValue || ''} isError={isError} + isHighlighted={isHighlighted} onFocus={handleFocus} onBlur={handleBlur} - onClick={handleClick} + onMouseDown={handleMouseDown} + onMouseEnter={handleMouseEnter} onChange={handleChange} /> @@ -675,6 +716,7 @@ const StyledGridInput = styled.input` height: 50px; padding: 20px; border: 2px solid ${({ isError }) => (isError ? 'red' : 'black')}; + background: ${({ isHighlighted }) => (isHighlighted ? '#dfd' : 'white')}; font-size: 20px; text-align: center;