From 03219c5089d302b336b25614e86ebe03c1d6e285 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Fri, 18 Jun 2021 19:01:18 -0700 Subject: [PATCH] Render nodes differently when zoomed out (#8) This changes it so that, when zoomed out, nodes will not render their bodies and will instead just render their title. Tags are also rendered until you zoom out far enough, then you just get the title. This also fixes long node titles causing the rename/change color/delete buttons to be cut off. --- loom-editor/src/Types.ts | 3 + loom-editor/src/components/NodeGraph.tsx | 7 +- .../{ => NodeWithBody}/NodeBody.test.tsx | 0 .../{ => NodeWithBody}/NodeBody.tsx | 0 .../NodeColorChooser.test.tsx | 0 .../{ => NodeWithBody}/NodeColorChooser.tsx | 4 +- .../{ => NodeWithBody}/NodeFooter.test.tsx | 4 +- .../{ => NodeWithBody}/NodeFooter.tsx | 8 +- .../{ => NodeWithBody}/NodeHeader.test.tsx | 2 +- .../{ => NodeWithBody}/NodeHeader.tsx | 9 +- .../NodeTagChooser.test.tsx | 6 +- .../{ => NodeWithBody}/NodeTagChooser.tsx | 10 +- .../NodeGraphView/NodeWithBody/index.test.tsx | 75 ++++++++ .../NodeGraphView/NodeWithBody/index.tsx | 84 +++++++++ .../NodeGraphView/ZoomedOutNode.test.tsx | 53 ++++++ .../NodeGraphView/ZoomedOutNode.tsx | 164 ++++++++++++++++++ .../components/NodeGraphView/index.test.tsx | 48 +++-- .../src/components/NodeGraphView/index.tsx | 95 ++++------ loom-editor/src/state/Reducer.test.ts | 7 + loom-editor/src/state/Reducer.ts | 5 + loom-editor/src/state/Selectors.ts | 1 + loom-editor/src/state/UiActions.ts | 6 + 22 files changed, 480 insertions(+), 111 deletions(-) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeBody.test.tsx (100%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeBody.tsx (100%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeColorChooser.test.tsx (100%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeColorChooser.tsx (93%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeFooter.test.tsx (94%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeFooter.tsx (89%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeHeader.test.tsx (96%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeHeader.tsx (88%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeTagChooser.test.tsx (94%) rename loom-editor/src/components/NodeGraphView/{ => NodeWithBody}/NodeTagChooser.tsx (88%) create mode 100644 loom-editor/src/components/NodeGraphView/NodeWithBody/index.test.tsx create mode 100644 loom-editor/src/components/NodeGraphView/NodeWithBody/index.tsx create mode 100644 loom-editor/src/components/NodeGraphView/ZoomedOutNode.test.tsx create mode 100644 loom-editor/src/components/NodeGraphView/ZoomedOutNode.tsx diff --git a/loom-editor/src/Types.ts b/loom-editor/src/Types.ts index 3bcf489..97f150c 100644 --- a/loom-editor/src/Types.ts +++ b/loom-editor/src/Types.ts @@ -27,4 +27,7 @@ export interface State { /** Node that is currently being focused on */ focusedNode?: string; + + /** Current zoom in the graph */ + currentZoom?: number; } diff --git a/loom-editor/src/components/NodeGraph.tsx b/loom-editor/src/components/NodeGraph.tsx index 262338c..cd5adea 100644 --- a/loom-editor/src/components/NodeGraph.tsx +++ b/loom-editor/src/components/NodeGraph.tsx @@ -16,7 +16,7 @@ import { openNode, setNodePosition } from "loom-common/EditorActions"; import { useYarnState } from "../state/YarnContext"; import NodeGraphView, { NodeSizePx } from "./NodeGraphView"; import { getNodes, getFocusedNode } from "../state/Selectors"; -import { setFocusedNode } from "../state/UiActions"; +import { setFocusedNode, setCurrentZoom } from "../state/UiActions"; const containerStyle = css` width: 100%; @@ -130,7 +130,6 @@ const NodeGraph: FunctionComponent = () => { height: containerNode.offsetHeight, }); - // @ts-ignore https://github.com/Microsoft/TypeScript/issues/28502 const resizeObserver = new ResizeObserver((entries) => { for (let entry of entries) { if (entry.contentRect) { @@ -163,6 +162,10 @@ const NodeGraph: FunctionComponent = () => { onDoubleClickNode={onNodeDoubleClicked} onNodePositionChange={onNodePositionChange} onClickGraph={() => dispatch(setFocusedNode(undefined))} + // @ts-expect-error until react-d3-graph is updated and https://github.com/DefinitelyTyped/DefinitelyTyped/pull/46632 is merged + onZoomChange={(previousZoom, newZoom) => + dispatch(setCurrentZoom(newZoom)) + } /> ); diff --git a/loom-editor/src/components/NodeGraphView/NodeBody.test.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeBody.test.tsx similarity index 100% rename from loom-editor/src/components/NodeGraphView/NodeBody.test.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeBody.test.tsx diff --git a/loom-editor/src/components/NodeGraphView/NodeBody.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeBody.tsx similarity index 100% rename from loom-editor/src/components/NodeGraphView/NodeBody.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeBody.tsx diff --git a/loom-editor/src/components/NodeGraphView/NodeColorChooser.test.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeColorChooser.test.tsx similarity index 100% rename from loom-editor/src/components/NodeGraphView/NodeColorChooser.test.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeColorChooser.test.tsx diff --git a/loom-editor/src/components/NodeGraphView/NodeColorChooser.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeColorChooser.tsx similarity index 93% rename from loom-editor/src/components/NodeGraphView/NodeColorChooser.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeColorChooser.tsx index c7880a5..1a79703 100644 --- a/loom-editor/src/components/NodeGraphView/NodeColorChooser.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeColorChooser.tsx @@ -4,8 +4,8 @@ import { FunctionComponent } from "react"; import { setNodeColor } from "loom-common/EditorActions"; -import { nodeColors } from "./index"; -import { buttonBase, nodeOverlayContainer } from "../../Styles"; +import { nodeColors } from "../index"; +import { buttonBase, nodeOverlayContainer } from "../../../Styles"; const containerStyle = css` ${nodeOverlayContainer} diff --git a/loom-editor/src/components/NodeGraphView/NodeFooter.test.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeFooter.test.tsx similarity index 94% rename from loom-editor/src/components/NodeGraphView/NodeFooter.test.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeFooter.test.tsx index bbe1038..47c459b 100644 --- a/loom-editor/src/components/NodeGraphView/NodeFooter.test.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeFooter.test.tsx @@ -1,8 +1,8 @@ import React from "react"; import { screen, fireEvent } from "@testing-library/react"; -import { renderWithProvider } from "../../utils/test-utils"; +import { renderWithProvider } from "../../../utils/test-utils"; -import { searchForTag } from "../../state/UiActions"; +import { searchForTag } from "../../../state/UiActions"; import NodeFooter from "./NodeFooter"; import { YarnNode } from "loom-common/YarnNode"; diff --git a/loom-editor/src/components/NodeGraphView/NodeFooter.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeFooter.tsx similarity index 89% rename from loom-editor/src/components/NodeGraphView/NodeFooter.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeFooter.tsx index b8562f5..e4fda6c 100644 --- a/loom-editor/src/components/NodeGraphView/NodeFooter.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeFooter.tsx @@ -2,11 +2,11 @@ import { css } from "@emotion/react/macro"; import { FunctionComponent } from "react"; -import { useYarnState } from "../../state/YarnContext"; -import { searchForTag } from "../../state/UiActions"; -import UiActionType from "../../state/UiActionType"; +import { useYarnState } from "../../../state/YarnContext"; +import { searchForTag } from "../../../state/UiActions"; +import UiActionType from "../../../state/UiActionType"; -import { ReactComponent as AddIcon } from "../../icons/add.svg"; +import { ReactComponent as AddIcon } from "../../../icons/add.svg"; import { YarnNode } from "loom-common/YarnNode"; const containerStyle = css` diff --git a/loom-editor/src/components/NodeGraphView/NodeHeader.test.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeHeader.test.tsx similarity index 96% rename from loom-editor/src/components/NodeGraphView/NodeHeader.test.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeHeader.test.tsx index 0c595fe..31810a8 100644 --- a/loom-editor/src/components/NodeGraphView/NodeHeader.test.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeHeader.test.tsx @@ -1,6 +1,6 @@ import React from "react"; import { screen, fireEvent } from "@testing-library/react"; -import { renderWithProvider } from "../../utils/test-utils"; +import { renderWithProvider } from "../../../utils/test-utils"; import { deleteNode, renameNode } from "loom-common/EditorActions"; diff --git a/loom-editor/src/components/NodeGraphView/NodeHeader.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeHeader.tsx similarity index 88% rename from loom-editor/src/components/NodeGraphView/NodeHeader.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeHeader.tsx index 138575d..d9bea08 100644 --- a/loom-editor/src/components/NodeGraphView/NodeHeader.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeHeader.tsx @@ -4,9 +4,9 @@ import { FunctionComponent } from "react"; import { deleteNode, renameNode } from "loom-common/EditorActions"; -import { ReactComponent as RenameIcon } from "../../icons/rename.svg"; -import { ReactComponent as TrashIcon } from "../../icons/trash.svg"; -import { ReactComponent as ColorIcon } from "../../icons/symbol-color.svg"; +import { ReactComponent as RenameIcon } from "../../../icons/rename.svg"; +import { ReactComponent as TrashIcon } from "../../../icons/trash.svg"; +import { ReactComponent as ColorIcon } from "../../../icons/symbol-color.svg"; const titleStyle = css` padding: 10px; @@ -19,6 +19,9 @@ const titleStyle = css` const titleLabelStyle = css` flex: 1; + + max-width: 94px; + overflow-x: hidden; `; const settingsButtonStyle = css` diff --git a/loom-editor/src/components/NodeGraphView/NodeTagChooser.test.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeTagChooser.test.tsx similarity index 94% rename from loom-editor/src/components/NodeGraphView/NodeTagChooser.test.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeTagChooser.test.tsx index 8ebdc98..2c32fc1 100644 --- a/loom-editor/src/components/NodeGraphView/NodeTagChooser.test.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeTagChooser.test.tsx @@ -1,12 +1,12 @@ import React from "react"; import { screen, fireEvent } from "@testing-library/react"; -import { renderWithProvider } from "../../utils/test-utils"; +import { renderWithProvider } from "../../../utils/test-utils"; import { toggleTagOnNode, promptForNewTags } from "loom-common/EditorActions"; import NodeTagChooser from "./NodeTagChooser"; -import { defaultState } from "../../state/YarnContext"; -import { State } from "../../Types"; +import { defaultState } from "../../../state/YarnContext"; +import { State } from "../../../Types"; describe("", () => { const node = { diff --git a/loom-editor/src/components/NodeGraphView/NodeTagChooser.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeTagChooser.tsx similarity index 88% rename from loom-editor/src/components/NodeGraphView/NodeTagChooser.tsx rename to loom-editor/src/components/NodeGraphView/NodeWithBody/NodeTagChooser.tsx index 5fda180..2890ec4 100644 --- a/loom-editor/src/components/NodeGraphView/NodeTagChooser.tsx +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/NodeTagChooser.tsx @@ -4,13 +4,13 @@ import { FunctionComponent } from "react"; import { YarnNode } from "loom-common/YarnNode"; -import { getNodes } from "../../state/Selectors"; -import { useYarnState } from "../../state/YarnContext"; +import { getNodes } from "../../../state/Selectors"; +import { useYarnState } from "../../../state/YarnContext"; -import { ReactComponent as PlusIcon } from "../../icons/add.svg"; -import { ReactComponent as CheckIcon } from "../../icons/check.svg"; +import { ReactComponent as PlusIcon } from "../../../icons/add.svg"; +import { ReactComponent as CheckIcon } from "../../../icons/check.svg"; -import { listItemBase, nodeOverlayContainer } from "../../Styles"; +import { listItemBase, nodeOverlayContainer } from "../../../Styles"; import { toggleTagOnNode, promptForNewTags } from "loom-common/EditorActions"; diff --git a/loom-editor/src/components/NodeGraphView/NodeWithBody/index.test.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/index.test.tsx new file mode 100644 index 0000000..c36835b --- /dev/null +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/index.test.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { renderWithProvider } from "../../../utils/test-utils"; +import { screen, fireEvent } from "@testing-library/react"; + +import { YarnNode } from "loom-common/YarnNode"; + +import NodeWithBody from "./"; + +describe("", () => { + const mockNode: YarnNode = { + title: "Mock Node", + body: "Some body", + tags: "cool tags", + }; + + const mockNodeColor = "blue"; + const mockNodeColorIsDark = true; + + it("renders", () => { + renderWithProvider( + + ); + }); + + it("renders no tags if there are none", () => { + const nodeWithNoTags = { + ...mockNode, + tags: "", + }; + renderWithProvider( + + ); + expect(screen.queryByTestId("node-graph-view-tags")).toBeNull(); + }); + + it("opens the color chooser", () => { + renderWithProvider( + + ); + + expect(screen.queryByTestId("node-title-color-chooser")).toBeNull(); + + fireEvent.click(screen.getByTitle("Change node color")); + + expect(screen.queryByTestId("node-title-color-chooser")).not.toBeNull(); + }); + + it("opens the tag chooser", () => { + renderWithProvider( + + ); + + expect(screen.queryByTestId("node-tag-chooser")).toBeNull(); + + fireEvent.click(screen.getByTitle("Manage node tags")); + + expect(screen.queryByTestId("node-tag-chooser")).not.toBeNull(); + }); +}); diff --git a/loom-editor/src/components/NodeGraphView/NodeWithBody/index.tsx b/loom-editor/src/components/NodeGraphView/NodeWithBody/index.tsx new file mode 100644 index 0000000..57a6040 --- /dev/null +++ b/loom-editor/src/components/NodeGraphView/NodeWithBody/index.tsx @@ -0,0 +1,84 @@ +import React, { Fragment, FunctionComponent, useState } from "react"; + +import { YarnNode } from "loom-common/YarnNode"; + +import NodeHeader from "./NodeHeader"; +import NodeFooter from "./NodeFooter"; +import NodeBody from "./NodeBody"; +import NodeColorChooser from "./NodeColorChooser"; +import NodeTagChooser from "./NodeTagChooser"; + +/** + * Render tha body of the node. + * If the color or tag chooser are open, those are rendered instead. + * (because of rendering bugs on Ubuntu, we have to render them instead of the body...) + * + * @param colorChooserOpen Whether or not the color chooser is open + * @param closeColorChooser Function to call to open the color chooser + * @param tagChooserOpen Whether or not the tag chooser is open + * @param closeTagChooser Function to call to close the tag chooser + * @param node Node to render body for + */ +const renderBody = ( + colorChooserOpen: boolean, + closeColorChooser: () => void, + tagChooserOpen: boolean, + closeTagChooser: () => void, + node: YarnNode +) => { + const { title, body } = node; + + if (colorChooserOpen) { + return ; + } + + if (tagChooserOpen) { + return ; + } + + return ; +}; + +interface NodeWithBodyProps { + yarnNode: YarnNode; + nodeColor: string; + nodeColorIsDark: boolean; +} + +const NodeWithBody: FunctionComponent = ({ + yarnNode, + nodeColor, + nodeColorIsDark, +}) => { + const [colorChooserOpen, setColorChooserOpen] = useState(false); + const [tagChooserOpen, setTagChooserOpen] = useState(false); + + const { title } = yarnNode; + + return ( + + setColorChooserOpen(!colorChooserOpen)} + /> + {renderBody( + colorChooserOpen, + () => setColorChooserOpen(false), + tagChooserOpen, + () => setTagChooserOpen(false), + yarnNode + )} + setTagChooserOpen(true)} + data-testid="node-graph-view-tags" + /> + + ); +}; + +export default NodeWithBody; diff --git a/loom-editor/src/components/NodeGraphView/ZoomedOutNode.test.tsx b/loom-editor/src/components/NodeGraphView/ZoomedOutNode.test.tsx new file mode 100644 index 0000000..2d9965b --- /dev/null +++ b/loom-editor/src/components/NodeGraphView/ZoomedOutNode.test.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { YarnNode } from "loom-common/YarnNode"; + +import ZoomedOutNode from "./ZoomedOutNode"; + +describe("", () => { + const mockNode: YarnNode = { + title: "Mock Node", + body: "Some body", + tags: "cool tags", + }; + + it("renders", () => { + render( + + ); + }); + + it("shows tags when not super zoomed out", () => { + render( + + ); + + // 2 because we have "cool tags" in our mock node + expect(screen.queryAllByTestId("zoomed-out-node-tag")).toHaveLength(2); + }); + + it("hides tags when really zoomed out", () => { + render( + + ); + + // these don't render if we're REALLY zoomed out + expect(screen.queryAllByTestId("zoomed-out-node-tag")).toHaveLength(0); + }); +}); diff --git a/loom-editor/src/components/NodeGraphView/ZoomedOutNode.tsx b/loom-editor/src/components/NodeGraphView/ZoomedOutNode.tsx new file mode 100644 index 0000000..4fd3cb8 --- /dev/null +++ b/loom-editor/src/components/NodeGraphView/ZoomedOutNode.tsx @@ -0,0 +1,164 @@ +/** @jsxImportSource @emotion/react */ +import { css } from "@emotion/react/macro"; +import { FunctionComponent } from "react"; + +import { YarnNode } from "loom-common/YarnNode"; + +// NOTE: This is also defined in NodeGraphView, but it cannot be imported because +// something funny happens when the package is built and it gets erased 💥 +export const NodeSizePx = 200; + +/** The zoom distance at which to switch to "real big' fonts */ +const ExtraZoomedOutNodeDistance = 0.2; + +const containerStyle = css` + grid-row: 1 / 3; /* fills the whole container */ + + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + + width: ${NodeSizePx}px; +`; + +const titleStyle = css` + font-size: 32px; + font-weight: 500; + + word-wrap: break-word; + display: inline-block; + + width: ${NodeSizePx}px; + max-height: 147px; +`; + +/** Used when we're "extra" zoomed out and only showing the title (not the tags) */ +const extraZoomedOutTitleStyle = css` + ${titleStyle} + + max-height: 100%; +`; + +const tagsContainerStyle = css` + width: ${NodeSizePx}px; + font-size: 22px; + margin-top: 15px; + + display: flex; + flex-wrap: wrap; +`; + +const tagStyle = css` + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + + padding-top: 2px; + padding-bottom: 2px; + padding-left: 5px; + padding-right: 5px; + + margin: 2px; +`; + +interface ZoomedOutNodeProps { + yarnNode: YarnNode; + nodeColor: string; + nodeColorIsDark: boolean; + currentZoom: number; +} + +/** + * This is a bad approximation of the correct font-size for when showing the + * "extra zoomed out" node. This isn't pretty but it works Good Enough™. + * + * @param titleLength Length of title + */ +const getFontSizePxForExtraZoomedOutTitle = (titleLength: number): number => { + if (titleLength > 40) { + return 36; + } + + if (titleLength > 30) { + return 45; + } + + if (titleLength >= 15) { + return 50; + } + + if (titleLength >= 12) { + return 70; + } + + return 75; +}; + +/** + * Get the style to use for the title string. + * + * @param extraZoomedOut Whether or not we're currently "extra" zoomed out + * @param nodeTitleLength The length of the title string + */ +const getTitleStyle = (extraZoomedOut: boolean, nodeTitleLength: number) => { + // not extra zoomed out; return the regular style + if (!extraZoomedOut) { + return titleStyle; + } + + // calculate our font size for the extra-zoomed-out title + return css` + ${extraZoomedOutTitleStyle} + font-size: ${getFontSizePxForExtraZoomedOutTitle(nodeTitleLength)}px; + `; +}; + +/** + * This is rendered instead of NodeWithBody when the graph is sufficiently zoomed out. + * By default, this shows tags. When zoomed out even further, it hides the tags. + */ +const ZoomedOutNode: FunctionComponent = ({ + yarnNode, + nodeColor, + nodeColorIsDark, + currentZoom, +}) => { + const fontColor = nodeColorIsDark ? "white" : "black"; + + const extraZoomedOut = currentZoom <= ExtraZoomedOutNodeDistance; + + return ( +
+
+ {yarnNode.title} +
+ + {/* Don't show tags if we're zoomed out far enough */} + {!extraZoomedOut && ( +
+ {yarnNode.tags.split(" ").map( + (tag) => + tag.length !== 0 && ( +
+ {tag} +
+ ) + )} +
+ )} +
+ ); +}; + +export default ZoomedOutNode; diff --git a/loom-editor/src/components/NodeGraphView/index.test.tsx b/loom-editor/src/components/NodeGraphView/index.test.tsx index 9bea9f6..cc621b0 100644 --- a/loom-editor/src/components/NodeGraphView/index.test.tsx +++ b/loom-editor/src/components/NodeGraphView/index.test.tsx @@ -1,9 +1,11 @@ import React from "react"; +import { screen } from "@testing-library/react"; + import { renderWithProvider } from "../../utils/test-utils"; -import { screen, fireEvent } from "@testing-library/react"; import NodeGraphView from "./"; import { YarnGraphNode } from "../NodeGraph"; +import { defaultState } from "../../state/YarnContext"; describe("", () => { const mockNode: YarnGraphNode = { @@ -19,36 +21,26 @@ describe("", () => { renderWithProvider(); }); - it("renders no tags if there are none", () => { - const nodeWithNoTags = { - ...mockNode, - yarnNode: { - ...mockNode.yarnNode, - tags: "", - }, - }; - renderWithProvider(); - expect(screen.queryByTestId("node-graph-view-tags")).toBeNull(); - }); - - it("opens the color chooser", () => { - renderWithProvider(); - - expect(screen.queryByTestId("node-title-color-chooser")).toBeNull(); - - fireEvent.click(screen.getByTitle("Change node color")); - - expect(screen.queryByTestId("node-title-color-chooser")).not.toBeNull(); - }); - - it("opens the tag chooser", () => { - renderWithProvider(); + describe("zooming", () => { + it("renders NodeWithBody when not zoomed out", () => { + renderWithProvider(, { + ...defaultState, + currentZoom: 1.0, + }); - expect(screen.queryByTestId("node-tag-chooser")).toBeNull(); + screen.getByTestId("node-body-text"); + expect(screen.queryByTestId("zoomed-out-node")).toBeNull(); + }); - fireEvent.click(screen.getByTitle("Manage node tags")); + it("renders ZoomedOutNode when zoomed out", () => { + renderWithProvider(, { + ...defaultState, + currentZoom: 0.05, + }); - expect(screen.queryByTestId("node-tag-chooser")).not.toBeNull(); + screen.getByTestId("zoomed-out-node"); + expect(screen.queryByTestId("node-body-text")).toBeNull(); + }); }); describe("searching", () => { diff --git a/loom-editor/src/components/NodeGraphView/index.tsx b/loom-editor/src/components/NodeGraphView/index.tsx index 1fefeeb..9902f91 100644 --- a/loom-editor/src/components/NodeGraphView/index.tsx +++ b/loom-editor/src/components/NodeGraphView/index.tsx @@ -1,6 +1,8 @@ /** @jsxImportSource @emotion/react */ import { css } from "@emotion/react/macro"; -import { FunctionComponent, useState } from "react"; +import { FunctionComponent } from "react"; + +import { YarnNode } from "loom-common/YarnNode"; import { useYarnState } from "../../state/YarnContext"; import { @@ -10,17 +12,15 @@ import { getSearchString, getCaseSensitivityEnabled, getRegexEnabled, + getCurrentZoom, } from "../../state/Selectors"; import { YarnGraphNode } from "../NodeGraph"; -import NodeHeader from "./NodeHeader"; -import NodeFooter from "./NodeFooter"; -import NodeBody from "./NodeBody"; -import NodeColorChooser from "./NodeColorChooser"; -import { YarnNode } from "loom-common/YarnNode"; -import NodeTagChooser from "./NodeTagChooser"; import { isDark } from "../../Util"; +import NodeWithBody from "./NodeWithBody"; +import ZoomedOutNode from "./ZoomedOutNode"; + /** CSS colors to cycle through for the "colorID" of a yarn node */ export const nodeColors = [ "#EBEBEB", @@ -34,9 +34,18 @@ export const nodeColors = [ "#000000", ]; -/** The width and height of the node's wrapper container */ +/** + * The width and height of the node's wrapper container + * + * NOTE: While this is exported, the export can NOT be used in + * CSS template strings or it will be ignored. This has something to do with + * the way that emotion creates its packaged CSS. + */ export const NodeSizePx = 200; +/** The zoom distance at which to switch from NodeWithBody to ZoomedOutNode */ +const ZoomedOutNodeDistance = 0.5; + const containerStyle = css` background: white; color: black; @@ -139,37 +148,6 @@ const isSearched = ( return searched; }; -/** - * Render tha body of the node. - * If the color or tag chooser are open, those are rendered instead. - * (because of rendering bugs on Ubuntu, we have to render them instead of the body...) - * - * @param colorChooserOpen Whether or not the color chooser is open - * @param closeColorChooser Function to call to open the color chooser - * @param tagChooserOpen Whether or not the tag chooser is open - * @param closeTagChooser Function to call to close the tag chooser - * @param node Node to render body for - */ -const renderBody = ( - colorChooserOpen: boolean, - closeColorChooser: () => void, - tagChooserOpen: boolean, - closeTagChooser: () => void, - node: YarnNode -) => { - const { title, body } = node; - - if (colorChooserOpen) { - return ; - } - - if (tagChooserOpen) { - return ; - } - - return ; -}; - interface NodeGraphViewProps { node: YarnGraphNode; } @@ -178,10 +156,8 @@ const NodeGraphView: FunctionComponent = ({ node: { yarnNode }, }) => { const [state] = useYarnState(); - const [colorChooserOpen, setColorChooserOpen] = useState(false); - const [tagChooserOpen, setTagChooserOpen] = useState(false); - const { colorID, title } = yarnNode; + const { colorID } = yarnNode; if (!state) { return null; @@ -193,6 +169,7 @@ const NodeGraphView: FunctionComponent = ({ const caseSensitivityEnabled = getCaseSensitivityEnabled(state); const regexEnabled = getRegexEnabled(state); const searchString = getSearchString(state); + const currentZoom = getCurrentZoom(state); // if we're searching for something, and this node matches that something, // then this will be true... if this is false, the node is rendered as "dimmed" @@ -206,6 +183,8 @@ const NodeGraphView: FunctionComponent = ({ regexEnabled ); + const zoomedOut = currentZoom && currentZoom <= ZoomedOutNodeDistance; + // grab the color by its ID and determine if it is dark or not const nodeColor = nodeColors[colorID || 0]; const nodeColorIsDark = isDark(nodeColor); @@ -219,26 +198,20 @@ const NodeGraphView: FunctionComponent = ({ searched ? "node-graph-view-searched" : "node-graph-view-not-searched" } > - setColorChooserOpen(!colorChooserOpen)} - /> - {renderBody( - colorChooserOpen, - () => setColorChooserOpen(false), - tagChooserOpen, - () => setTagChooserOpen(false), - yarnNode + {zoomedOut ? ( + + ) : ( + )} - setTagChooserOpen(true)} - data-testid="node-graph-view-tags" - /> ); }; diff --git a/loom-editor/src/state/Reducer.test.ts b/loom-editor/src/state/Reducer.test.ts index a5687ec..d812a71 100644 --- a/loom-editor/src/state/Reducer.test.ts +++ b/loom-editor/src/state/Reducer.test.ts @@ -11,6 +11,7 @@ import { searchForTag, setSearchCaseSensitive, setSearchRegexEnabled, + setCurrentZoom, } from "./UiActions"; describe("Reducer", () => { @@ -142,4 +143,10 @@ describe("Reducer", () => { expect(reduced.search.searchingTags).toBe(true); }); }); + + it("handles UiActionType.SetCurrentZoom", () => { + expect(startState.currentZoom).toBeUndefined(); + const reduced = reducer(startState, setCurrentZoom(0.5)); + expect(reduced.currentZoom).toBe(0.5); + }); }); diff --git a/loom-editor/src/state/Reducer.ts b/loom-editor/src/state/Reducer.ts index 3fbc57c..39ead71 100644 --- a/loom-editor/src/state/Reducer.ts +++ b/loom-editor/src/state/Reducer.ts @@ -111,6 +111,11 @@ const reducer = (state: State, action: EditorActions | UiActions): State => { }; case UiMessageTypes.SearchForTag: return searchForTag(state, action.payload.tag); + case UiMessageTypes.SetCurrentZoom: + return { + ...state, + currentZoom: action.payload.zoom, + }; default: return state; } diff --git a/loom-editor/src/state/Selectors.ts b/loom-editor/src/state/Selectors.ts index 1d19858..0458426 100644 --- a/loom-editor/src/state/Selectors.ts +++ b/loom-editor/src/state/Selectors.ts @@ -11,3 +11,4 @@ export const getRegexEnabled = (state: State) => state.search.regexEnabled; export const getSearchString = (state: State) => state.search.searchString; export const getFocusedNode = (state?: State) => state?.focusedNode; +export const getCurrentZoom = (state?: State) => state?.currentZoom; diff --git a/loom-editor/src/state/UiActions.ts b/loom-editor/src/state/UiActions.ts index 9ec2da1..666553a 100644 --- a/loom-editor/src/state/UiActions.ts +++ b/loom-editor/src/state/UiActions.ts @@ -25,6 +25,9 @@ export enum UiMessageTypes { /** Search for a specific tag */ SearchForTag = "SearchForTag", + + /** Set the current zoom level of the graph */ + SetCurrentZoom = "SetCurrentZoom", } export const setSearchingNodeTitles = (searchingTitle: boolean) => @@ -50,3 +53,6 @@ export const setFocusedNode = (nodeTitle?: string) => export const searchForTag = (tag: string) => action(UiMessageTypes.SearchForTag, { tag }); + +export const setCurrentZoom = (zoom: number) => + action(UiMessageTypes.SetCurrentZoom, { zoom });