Split gridCount into independent rows and cols configs

This commit is contained in:
Max Goodhart
2025-06-22 23:57:56 -07:00
parent 2dddb8f8e8
commit 53a9804365
5 changed files with 67 additions and 50 deletions

View File

@@ -256,10 +256,11 @@ export function ControlUI({
role, role,
} = connection } = connection
const { const {
gridCount, cols,
rows,
width: windowWidth, width: windowWidth,
height: windowHeight, height: windowHeight,
} = config ?? { gridCount: null, width: null, height: null } } = config ?? { cols: null, rows: null, width: null, height: null }
const [showDebug, setShowDebug] = useState(false) const [showDebug, setShowDebug] = useState(false)
const handleChangeShowDebug = useCallback< const handleChangeShowDebug = useCallback<
@@ -311,20 +312,24 @@ export function ControlUI({
const [hoveringIdx, setHoveringIdx] = useState<number>() const [hoveringIdx, setHoveringIdx] = useState<number>()
const updateHoveringIdx = useCallback( const updateHoveringIdx = useCallback(
(ev: MouseEvent) => { (ev: MouseEvent) => {
if (gridCount == null || !(ev.currentTarget instanceof HTMLElement)) { if (
cols == null ||
rows == null ||
!(ev.currentTarget instanceof HTMLElement)
) {
return return
} }
const { width, height, left, top } = const { width, height, left, top } =
ev.currentTarget.getBoundingClientRect() ev.currentTarget.getBoundingClientRect()
const x = Math.floor(ev.clientX - left) const x = Math.floor(ev.clientX - left)
const y = Math.floor(ev.clientY - top) const y = Math.floor(ev.clientY - top)
const spaceWidth = width / gridCount const spaceWidth = width / cols
const spaceHeight = height / gridCount const spaceHeight = height / rows
const idx = const idx =
Math.floor(y / spaceHeight) * gridCount + Math.floor(x / spaceWidth) Math.floor(y / spaceHeight) * cols + Math.floor(x / spaceWidth)
setHoveringIdx(idx) setHoveringIdx(idx)
}, },
[setHoveringIdx, gridCount], [setHoveringIdx, cols, rows],
) )
const [dragStart, setDragStart] = useState<number | undefined>() const [dragStart, setDragStart] = useState<number | undefined>()
const handleDragStart = useCallback( const handleDragStart = useCallback(
@@ -347,14 +352,19 @@ export function ControlUI({
) )
useLayoutEffect(() => { useLayoutEffect(() => {
function endDrag() { function endDrag() {
if (dragStart == null || gridCount == null || hoveringIdx == null) { if (
dragStart == null ||
cols == null ||
rows == null ||
hoveringIdx == null
) {
return return
} }
stateDoc.transact(() => { stateDoc.transact(() => {
const viewsState = stateDoc.getMap<Y.Map<string | undefined>>('views') const viewsState = stateDoc.getMap<Y.Map<string | undefined>>('views')
const streamId = viewsState.get(String(dragStart))?.get('streamId') const streamId = viewsState.get(String(dragStart))?.get('streamId')
for (let idx = 0; idx < gridCount ** 2; idx++) { for (let idx = 0; idx < cols * rows; idx++) {
if (idxInBox(gridCount, dragStart, hoveringIdx, idx)) { if (idxInBox(cols, dragStart, hoveringIdx, idx)) {
viewsState.get(String(idx))?.set('streamId', streamId) viewsState.get(String(idx))?.set('streamId', streamId)
} }
} }
@@ -462,7 +472,7 @@ export function ControlUI({
const handleClickId = useCallback( const handleClickId = useCallback(
(streamId: string) => { (streamId: string) => {
if (gridCount == null || sharedState == null) { if (cols == null || rows == null || sharedState == null) {
return return
} }
@@ -477,7 +487,7 @@ export function ControlUI({
return return
} }
const availableIdx = range(gridCount * gridCount).find( const availableIdx = range(cols * rows).find(
(i) => !sharedState.views[i].streamId, (i) => !sharedState.views[i].streamId,
) )
if (availableIdx === undefined) { if (availableIdx === undefined) {
@@ -485,7 +495,7 @@ export function ControlUI({
} }
handleSetView(availableIdx, streamId) handleSetView(availableIdx, streamId)
}, },
[gridCount, sharedState, focusedInputIdx], [cols, rows, sharedState, focusedInputIdx],
) )
const handleChangeCustomStream = useCallback( const handleChangeCustomStream = useCallback(
@@ -660,7 +670,7 @@ export function ControlUI({
/> />
)} )}
<StyledDataContainer isConnected={isConnected}> <StyledDataContainer isConnected={isConnected}>
{gridCount && ( {cols != null && rows != null && (
<StyledGridContainer <StyledGridContainer
className="grid" className="grid"
onMouseMove={updateHoveringIdx} onMouseMove={updateHoveringIdx}
@@ -668,21 +678,21 @@ export function ControlUI({
windowHeight={windowHeight} windowHeight={windowHeight}
> >
<StyledGridInputs> <StyledGridInputs>
{range(0, gridCount).map((y) => {range(0, rows).map((y) =>
range(0, gridCount).map((x) => { range(0, cols).map((x) => {
const idx = gridCount * y + x const idx = cols * y + x
const { streamId } = sharedState?.views?.[idx] ?? {} const { streamId } = sharedState?.views?.[idx] ?? {}
const isDragHighlighted = const isDragHighlighted =
dragStart != null && dragStart != null &&
hoveringIdx != null && hoveringIdx != null &&
idxInBox(gridCount, dragStart, hoveringIdx, idx) idxInBox(cols, dragStart, hoveringIdx, idx)
return ( return (
<GridInput <GridInput
style={{ style={{
width: `${100 / gridCount}%`, width: `${100 / cols}%`,
height: `${100 / gridCount}%`, height: `${100 / rows}%`,
left: `${(100 * x) / gridCount}%`, left: `${(100 * x) / cols}%`,
top: `${(100 * y) / gridCount}%`, top: `${(100 * y) / rows}%`,
}} }}
idx={idx} idx={idx}
spaceValue={streamId ?? ''} spaceValue={streamId ?? ''}

View File

@@ -16,8 +16,8 @@ export interface ViewContent {
export type ViewContentMap = Map<string, ViewContent> export type ViewContentMap = Map<string, ViewContent>
export function boxesFromViewContentMap( export function boxesFromViewContentMap(
width: number, cols: number,
height: number, rows: number,
viewContentMap: ViewContentMap, viewContentMap: ViewContentMap,
) { ) {
const boxes = [] const boxes = []
@@ -28,7 +28,7 @@ export function boxesFromViewContentMap(
y: number, y: number,
content: ViewContent | undefined, content: ViewContent | undefined,
) { ) {
const checkIdx = width * y + x const checkIdx = cols * y + x
return ( return (
!visited.has(checkIdx) && !visited.has(checkIdx) &&
isEqual(viewContentMap.get(String(checkIdx)), content) isEqual(viewContentMap.get(String(checkIdx)), content)
@@ -36,28 +36,28 @@ export function boxesFromViewContentMap(
} }
function findLargestBox(x: number, y: number) { function findLargestBox(x: number, y: number) {
const idx = width * y + x const idx = cols * y + x
const spaces = [idx] const spaces = [idx]
const content = viewContentMap.get(String(idx)) const content = viewContentMap.get(String(idx))
let maxY let maxY
for (maxY = y + 1; maxY < height; maxY++) { for (maxY = y + 1; maxY < rows; maxY++) {
if (!isPosContent(x, maxY, content)) { if (!isPosContent(x, maxY, content)) {
break break
} }
spaces.push(width * maxY + x) spaces.push(cols * maxY + x)
} }
let cx = x let cx = x
let cy = y let cy = y
scan: for (cx = x + 1; cx < width; cx++) { scan: for (cx = x + 1; cx < cols; cx++) {
for (cy = y; cy < maxY; cy++) { for (cy = y; cy < maxY; cy++) {
if (!isPosContent(cx, cy, content)) { if (!isPosContent(cx, cy, content)) {
break scan break scan
} }
} }
for (let cy = y; cy < maxY; cy++) { for (let cy = y; cy < maxY; cy++) {
spaces.push(width * cy + cx) spaces.push(cols * cy + cx)
} }
} }
const w = cx - x const w = cx - x
@@ -66,9 +66,9 @@ export function boxesFromViewContentMap(
return { content, x, y, w, h, spaces } return { content, x, y, w, h, spaces }
} }
for (let y = 0; y < width; y++) { for (let y = 0; y < rows; y++) {
for (let x = 0; x < height; x++) { for (let x = 0; x < cols; x++) {
const idx = width * y + x const idx = cols * y + x
if (visited.has(idx) || viewContentMap.get(String(idx)) === undefined) { if (visited.has(idx) || viewContentMap.get(String(idx)) === undefined) {
continue continue
} }
@@ -84,21 +84,21 @@ export function boxesFromViewContentMap(
return boxes return boxes
} }
export function idxToCoords(gridCount: number, idx: number) { export function idxToCoords(cols: number, idx: number) {
const x = idx % gridCount const x = idx % cols
const y = Math.floor(idx / gridCount) const y = Math.floor(idx / cols)
return { x, y } return { x, y }
} }
export function idxInBox( export function idxInBox(
gridCount: number, cols: number,
start: number, start: number,
end: number, end: number,
idx: number, idx: number,
) { ) {
const { x: startX, y: startY } = idxToCoords(gridCount, start) const { x: startX, y: startY } = idxToCoords(cols, start)
const { x: endX, y: endY } = idxToCoords(gridCount, end) const { x: endX, y: endY } = idxToCoords(cols, end)
const { x, y } = idxToCoords(gridCount, idx) const { x, y } = idxToCoords(cols, idx)
const lowX = Math.min(startX, endX) const lowX = Math.min(startX, endX)
const highX = Math.max(startX, endX) const highX = Math.max(startX, endX)
const lowY = Math.min(startY, endY) const lowY = Math.min(startY, endY)

View File

@@ -2,7 +2,8 @@ import type { ViewContent, ViewPos } from './geometry.ts'
import type { StreamwallRole } from './roles.ts' import type { StreamwallRole } from './roles.ts'
export interface StreamWindowConfig { export interface StreamWindowConfig {
gridCount: number cols: number
rows: number
width: number width: number
height: number height: number
x?: number x?: number

View File

@@ -198,11 +198,11 @@ export default class StreamWindow extends EventEmitter<StreamWindowEventMap> {
} }
setViews(viewContentMap: ViewContentMap, streams: StreamList) { setViews(viewContentMap: ViewContentMap, streams: StreamList) {
const { width, height, gridCount } = this.config const { width, height, cols, rows } = this.config
const spaceWidth = Math.floor(width / gridCount) const spaceWidth = Math.floor(width / cols)
const spaceHeight = Math.floor(height / gridCount) const spaceHeight = Math.floor(height / rows)
const { win, views } = this const { win, views } = this
const boxes = boxesFromViewContentMap(gridCount, gridCount, viewContentMap) const boxes = boxesFromViewContentMap(cols, rows, viewContentMap)
const remainingBoxes = new Set(boxes) const remainingBoxes = new Set(boxes)
const unusedViews = new Set(views.values()) const unusedViews = new Set(views.values())
const viewsToDisplay = [] const viewsToDisplay = []

View File

@@ -34,7 +34,8 @@ const SENTRY_DSN =
export interface StreamwallConfig { export interface StreamwallConfig {
help: boolean help: boolean
grid: { grid: {
count: number cols: number
rows: number
} }
window: { window: {
x?: number x?: number
@@ -97,8 +98,12 @@ function parseArgs(): StreamwallConfig {
.config('config', (configPath) => { .config('config', (configPath) => {
return TOML.parse(fs.readFileSync(configPath, 'utf-8')) return TOML.parse(fs.readFileSync(configPath, 'utf-8'))
}) })
.group(['grid.count'], 'Grid dimensions') .group(['grid.cols', 'grid.rows'], 'Grid dimensions')
.option('grid.count', { .option('grid.cols', {
number: true,
default: 3,
})
.option('grid.rows', {
number: true, number: true,
default: 3, default: 3,
}) })
@@ -267,7 +272,8 @@ async function main(argv: ReturnType<typeof parseArgs>) {
const overlayStreamData = new LocalStreamData() const overlayStreamData = new LocalStreamData()
const streamWindowConfig = { const streamWindowConfig = {
gridCount: argv.grid.count, cols: argv.grid.cols,
rows: argv.grid.rows,
width: argv.window.width, width: argv.window.width,
height: argv.window.height, height: argv.window.height,
x: argv.window.x, x: argv.window.x,
@@ -337,7 +343,7 @@ async function main(argv: ReturnType<typeof parseArgs>) {
) )
stateDoc.transact(() => { stateDoc.transact(() => {
for (let i = 0; i < argv.grid.count ** 2; i++) { for (let i = 0; i < argv.grid.cols * argv.grid.rows; i++) {
if (viewsState.has(String(i))) { if (viewsState.has(String(i))) {
continue continue
} }