function* handleSelectionResized() { while(true) { let { workspaceId, anchor, originalBounds, newBounds, sourceEvent } = (yield take(RESIZER_PATH_MOUSE_MOVED)) as ResizerPathMoved; const state: ApplicationState = yield select(); const workspace = getWorkspaceById(state, workspaceId); // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist const currentBounds = getWorkspaceSelectionBounds(workspace); const keepAspectRatio = sourceEvent.shiftKey; const keepCenter = sourceEvent.altKey; if (keepCenter) { // newBounds = keepBoundsCenter(newBounds, bounds, anchor); } if (keepAspectRatio) { newBounds = keepBoundsAspectRatio(newBounds, originalBounds, anchor, keepCenter ? { left: 0.5, top: 0.5 } : anchor); } for (const item of getBoundedWorkspaceSelection(workspace)) { const innerBounds = getWorkspaceItemBounds(item, workspace); const scaledBounds = scaleInnerBounds(currentBounds, currentBounds, newBounds); yield put(resized(item.$id, item.$type, scaleInnerBounds(innerBounds, currentBounds, newBounds), workspace.targetCSSSelectors)); } } }
const stageReducer = (state: ApplicationState, event: BaseEvent) => { switch(event.type) { case VISUAL_EDITOR_WHEEL: { const { workspaceId, metaKey, ctrlKey, deltaX, deltaY, canvasHeight, canvasWidth } = event as StageWheel; const workspace = getWorkspaceById(state, workspaceId); if (workspace.stage.fullScreen) { return state; } let translate = getStageTranslate(workspace.stage); if (metaKey || ctrlKey) { translate = centerTransformZoom(translate, boundsFromRect({ width: canvasWidth, height: canvasHeight }), clamp(translate.zoom + translate.zoom * deltaY / ZOOM_SENSITIVITY, MIN_ZOOM, MAX_ZOOM), workspace.stage.mousePosition); } else { translate = { ...translate, left: translate.left - deltaX, top: translate.top - deltaY }; } return updateWorkspaceStage(state, workspace.$id, { smooth: false, translate }); } case TOGGLE_TOOLS_SHORTCUT_PRESSED: { const workspace = getSelectedWorkspace(state); return updateWorkspaceStage(state, workspace.$id, { showTools: workspace.stage.showTools == null ? false : !workspace.stage.showTools }) } case STAGE_TOOL_EDIT_TEXT_KEY_DOWN: { const { sourceEvent, nodeId } = event as StageToolEditTextKeyDown; if (sourceEvent.key === "Escape") { // const workspace = getSyntheticNodeWorkspace(state, nodeId); // state = setWorkspaceSelection(state, workspace.$id, getStructReference(getNestedObjectById(nodeId, getNodeArtboard(nodeId, state).document))); // state = updateWorkspaceStage(state, workspace.$id, { // secondarySelection: false // }); } return state; } case RESIZER_MOVED: { const { point, workspaceId, point: newPoint } = event as ResizerMoved; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspace.$id, { movingOrResizing: true }); const translate = getStageTranslate(workspace.stage); const selectionBounds = getWorkspaceSelectionBounds(workspace); for (const item of getBoundedWorkspaceSelection(workspace)) { const itemBounds = getWorkspaceItemBounds(item, workspace); // skip moving window if in full screen mode if (workspace.stage.fullScreen && workspace.stage.fullScreen.artboardId === item.$id) { break; } const newBounds = roundBounds(scaleInnerBounds(itemBounds, selectionBounds, moveBounds(selectionBounds, newPoint))); if (item.$type === ARTBOARD) { state = updateArtboard(state, item.$id, { bounds: newBounds }); } } return state; } case RESIZER_PATH_MOUSE_MOVED: { let { workspaceId, anchor, originalBounds, newBounds, sourceEvent } = event as ResizerPathMoved; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspace.$id, { movingOrResizing: true }); // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist const currentBounds = getWorkspaceSelectionBounds(workspace); const keepAspectRatio = sourceEvent.shiftKey; const keepCenter = sourceEvent.altKey; if (keepCenter) { // newBounds = keepBoundsCenter(newBounds, bounds, anchor); } if (keepAspectRatio) { newBounds = keepBoundsAspectRatio(newBounds, originalBounds, anchor, keepCenter ? { left: 0.5, top: 0.5 } : anchor); } for (const item of getBoundedWorkspaceSelection(workspace)) { const innerBounds = getWorkspaceItemBounds(item, workspace); const scaledBounds = scaleInnerBounds(currentBounds, currentBounds, newBounds); if (item.$type === ARTBOARD) { state = updateArtboard(state, item.$id, { bounds: scaleInnerBounds(innerBounds, currentBounds, newBounds) }) } } return state; } case RESIZER_PATH_MOUSE_STOPPED_MOVING: case RESIZER_STOPPED_MOVING: { const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspace.$id, { movingOrResizing: false }); return state; } case ARTBOARD_FOCUSED: { const { artboardId } = event as ArtboardFocused; return selectAndCenterArtboard(state, getArtboardById(artboardId, state)); } case STAGE_TOOL_OVERLAY_MOUSE_LEAVE: { const { sourceEvent } = event as StageToolOverlayMouseMoved; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [] }); } case CSS_DECLARATION_TITLE_MOUSE_ENTER: { const { artboardId, ruleId } = event as CSSDeclarationTitleMouseLeaveEnter; const artboard = getArtboardById(artboardId, state); // TODO return state; // const { selectorText }: SEnvCSSStyleRuleInterface = getNestedObjectById(ruleId, artboard.document); // return updateWorkspace(state, state.selectedWorkspaceId, { // hoveringRefs: getMatchingElements(artboard, selectorText).map((element) => [ // element.$type, // element.$id // ]) as [[string, string]] // }); } case CSS_DECLARATION_TITLE_MOUSE_LEAVE: { const { artboardId, ruleId } = event as CSSDeclarationTitleMouseLeaveEnter; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [] }); } case BREADCRUMB_ITEM_CLICKED: { const { artboardId, nodeId } = event as BreadcrumbItemClicked; const artboard = getArtboardById(artboardId, state); const node = getNestedObjectById(nodeId, artboard.document) as SlimBaseNode; const workspace = getArtboardWorkspace(artboard.$id, state); return setWorkspaceSelection(state, workspace.$id, [node.type, node.id]); } case BREADCRUMB_ITEM_MOUSE_ENTER: { const { artboardId, nodeId } = event as BreadcrumbItemMouseEnterLeave; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [[SlimVMObjectType.ELEMENT, nodeId]] }); } case BREADCRUMB_ITEM_MOUSE_LEAVE: { const { artboardId, nodeId } = event as BreadcrumbItemMouseEnterLeave; return updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: [] }); } case STAGE_MOUNTED: { const { element } = event as StageMounted; const { width = 400, height = 300 } = element.getBoundingClientRect() || {}; const workspaceId = state.selectedWorkspaceId; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStage(state, workspaceId, { container: element }); // do not center if in full screen mode if (workspace.stage.fullScreen) { return updateArtboardSize(state, workspace.stage.fullScreen.artboardId, width, height); } return centerSelectedWorkspace(state); }; case STAGE_TOOL_OVERLAY_MOUSE_PAN_START: { const { artboardId } = event as StageToolOverlayMousePanStart; const workspace = getArtboardWorkspace(artboardId, state); return updateWorkspaceStage(state, workspace.$id, { panning: true }); } case STAGE_TOOL_OVERLAY_MOUSE_PAN_END: { const { artboardId } = event as StageToolOverlayMousePanEnd; const workspace = getArtboardWorkspace(artboardId, state) return updateWorkspaceStage(state, workspace.$id, { panning: false }); } case STAGE_MOUSE_MOVED: { const { sourceEvent: { pageX, pageY }} = event as WrappedEvent<React.MouseEvent<any>>; state = updateWorkspaceStage(state, state.selectedWorkspaceId, { mousePosition: { left: pageX, top: pageY } }); const workspace = getSelectedWorkspace(state); // TODO - in the future, we'll probably want to be able to highlight hovered nodes as the user is moving an element around to indicate where // they can drop the element. const targetRef = workspace.stage.movingOrResizing ? null : getStageToolMouseNodeTargetReference(state, event as StageToolOverlayMouseMoved); state = updateWorkspace(state, state.selectedWorkspaceId, { hoveringRefs: targetRef ? [targetRef] : [] }); return state; }; case STAGE_MOUSE_CLICKED: { const { sourceEvent } = event as StageToolNodeOverlayClicked; if (/textarea|input/i.test((sourceEvent.target as Element).nodeName)) { return state; } // alt key opens up a new link const altKey = sourceEvent.altKey; const workspace = getSelectedWorkspace(state); state = updateWorkspaceStageSmoothing(state, workspace); // do not allow selection while window is panning (scrolling) if (workspace.stage.panning || workspace.stage.movingOrResizing) return state; const targetRef = getStageToolMouseNodeTargetReference(state, event as StageToolNodeOverlayClicked); if (!targetRef) { return state; } if (!altKey) { state = handleArtboardSelectionFromAction(state, targetRef, event as StageToolNodeOverlayClicked); state = updateWorkspaceStage(state, workspace.$id, { secondarySelection: false }); return state; } return state; } case STAGE_TOOL_OVERLAY_MOUSE_DOUBLE_CLICKED: { const { sourceEvent, artboardId } = event as StageToolNodeOverlayClicked; const workspace = getArtboardWorkspace(artboardId, state); const targetRef = getStageToolMouseNodeTargetReference(state, event as StageToolNodeOverlayClicked); if (!targetRef) return state; state = updateWorkspaceStage(state, workspace.$id, { secondarySelection: true }); state = setWorkspaceSelection(state, workspace.$id, targetRef); return state; } case ARTBOARD_SELECTION_SHIFTED: { const { artboardId } = event as ArtboardSelectionShifted; return selectAndCenterArtboard(state, getArtboardById(artboardId, state)); } case SELECTOR_DOUBLE_CLICKED: { const { sourceEvent, item } = event as SelectorDoubleClicked; const workspace = getSyntheticNodeWorkspace(state, item.$id); state = updateWorkspaceStage(state, workspace.$id, { secondarySelection: true }); state = setWorkspaceSelection(state, workspace.$id, getStructReference(item)); return state; } case ARTBOARD_SCROLL: { const { artboardId, scrollPosition } = event as ArtboardScroll; return updateArtboard(state, artboardId, { scrollPosition }); } case WORKSPACE_DELETION_SELECTED: { const { workspaceId } = event as WorkspaceSelectionDeleted; state = clearWorkspaceSelection(state, workspaceId); return state; } case STAGE_TOOL_ARTBOARD_TITLE_CLICKED: { state = updateWorkspaceStageSmoothing(state); return handleArtboardSelectionFromAction(state, getStructReference(getArtboardById((event as ArtboardPaneRowClicked).artboardId, state)), event as ArtboardPaneRowClicked); } case STAGE_TOOL_WINDOW_BACKGROUND_CLICKED: { const workspace = getSelectedWorkspace(state); return clearWorkspaceSelection(state, workspace.$id); } } return state; }