addons/isl/src/RenderDag.tsxblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {ReactNode} from 'react';
b69ab319import type {Dag, DagCommitInfo} from './dag/dag';
b69ab3110import type {ExtendedGraphRow} from './dag/render';
b69ab3111import type {HashSet} from './dag/set';
b69ab3112
b69ab3113import React from 'react';
b69ab3114import {AnimatedReorderGroup} from './AnimatedReorderGroup';
b69ab3115import {AvatarPattern} from './Avatar';
b69ab3116import {YouAreHereLabel} from './YouAreHereLabel';
b69ab3117import {LinkLine, NodeLine, PadLine} from './dag/render';
b69ab3118
b69ab3119import './RenderDag.css';
b69ab3120
b69ab3121/* eslint no-bitwise: 0 */
b69ab3122
b69ab3123export type RenderDagProps = {
b69ab3124 /** The dag to use */
b69ab3125 dag: Dag;
b69ab3126
b69ab3127 /** If set, render a subset. Otherwise, all commits are rendered. */
b69ab3128 subset?: HashSet;
b69ab3129
b69ab3130 /** Should "anonymous" parents (rendered as "~" in CLI) be ignored? */
b69ab3131 ignoreAnonymousParents?: boolean;
b69ab3132} & React.HTMLAttributes<HTMLDivElement> &
b69ab3133 RenderFunctionProps;
b69ab3134
b69ab3135type RenderFunctionProps = {
b69ab3136 /**
b69ab3137 * How to render a commit.
b69ab3138 *
b69ab3139 * To avoid re-rendering, pass a "static" (ex. not a closure) function,
b69ab3140 * then use hooks (ex. recoil selector) to trigger re-rendering inside
b69ab3141 * the static function.
b69ab3142 */
b69ab3143 renderCommit?: (info: DagCommitInfo) => JSX.Element;
b69ab3144
b69ab3145 /**
b69ab3146 * How to render extra stuff below a commit. Default: nothing.
b69ab3147 *
b69ab3148 * To avoid re-rendering, pass a "static" (ex. not a closure) function,
b69ab3149 * then use hooks (ex. recoil selector) to trigger re-rendering inside
b69ab3150 * the static function.
b69ab3151 */
b69ab3152 renderCommitExtras?: (info: DagCommitInfo, row: ExtendedGraphRow) => null | JSX.Element;
b69ab3153
b69ab3154 /**
b69ab3155 * How to render a "glyph" (ex. "o", "x", "@").
b69ab3156 * This should return an SVG element.
b69ab3157 * The SVG viewbox is (-10,-10) to (10,10) (20px * 20px).
b69ab3158 * Default: defaultRenderGlyphSvg, draw a circle.
b69ab3159 *
b69ab3160 * To avoid re-rendering, pass a "static" (ex. not a closure) function,
b69ab3161 * then use hooks (ex. recoil selector) to trigger re-rendering inside
b69ab3162 * the static function.
b69ab3163 */
b69ab3164 renderGlyph?: (info: DagCommitInfo) => RenderGlyphResult;
b69ab3165
b69ab3166 /**
b69ab3167 * Get extra props for the DivRow for the given commit.
b69ab3168 * This can be used to tweak styles like selection background, border.
b69ab3169 * This should be a static-ish function to avoid re-rendering. Inside the function,
b69ab3170 * it can use hooks to fetch extra state.
b69ab3171 */
b69ab3172 useExtraCommitRowProps?: (info: DagCommitInfo) => React.HTMLAttributes<HTMLDivElement> | void;
b69ab3173};
b69ab3174
b69ab3175/**
b69ab3176 * - 'inside-tile': Inside a <Tile />. Must be a svg element. Size decided by <Tile />.
b69ab3177 * - 'replace-tile': Replace the <Tile /> with the rendered result. Size decided by the
b69ab3178 * rendered result. Can be other elements not just svg. Useful for "You are here".
b69ab3179 */
b69ab3180export type RenderGlyphResult = ['inside-tile', JSX.Element] | ['replace-tile', JSX.Element];
b69ab3181
b69ab3182/**
b69ab3183 * Renders a dag. Calculate and render the edges, aka. the left side:
b69ab3184 *
b69ab3185 * o +--------+
b69ab3186 * | | commit |
b69ab3187 * | +--------+
b69ab3188 * |
b69ab3189 * | o +--------+
b69ab3190 * |/ | commit |
b69ab3191 * o +--------+
b69ab3192 * :\
b69ab3193 * : o +--------+
b69ab3194 * : | commit |
b69ab3195 * : +--------+
b69ab3196 * :
b69ab3197 * o +--------+
b69ab3198 * | commit |
b69ab3199 * +--------+
b69ab31100 *
b69ab31101 * The callsite can customize:
b69ab31102 * - What "dag" and what subset of commits to render.
b69ab31103 * - How to render each "commit" (the boxes above).
b69ab31104 * - How to render the glyph (the "o").
b69ab31105 *
b69ab31106 * For a commit with `info.isYouAreHere` set, the "commit" body
b69ab31107 * will be positioned at the right of the "pad" line, not the
b69ab31108 * "node" line, and the default "o" rendering logic will render
b69ab31109 * a blue badge instead.
b69ab31110 *
b69ab31111 * See `DagListProps` for customization options.
b69ab31112 *
b69ab31113 * This component is intended to be used in multiple places,
b69ab31114 * ex. the main dag, "mutation dag", context-menu sub-dag, etc.
b69ab31115 * So it should avoid depending on Recoil states.
b69ab31116 */
b69ab31117export function RenderDag(props: RenderDagProps) {
b69ab31118 const {
b69ab31119 dag,
b69ab31120 subset,
b69ab31121 renderCommit,
b69ab31122 renderCommitExtras,
b69ab31123 renderGlyph = defaultRenderGlyph,
b69ab31124 useExtraCommitRowProps,
b69ab31125 className,
b69ab31126 ...restProps
b69ab31127 } = props;
b69ab31128
b69ab31129 const rows = dag.renderToRows(subset);
b69ab31130 const authors = new Set<string>(
b69ab31131 rows.flatMap(([info]) => (info.phase === 'draft' && info.author.length > 0 ? info.author : [])),
b69ab31132 );
b69ab31133
b69ab31134 const renderedRows: Array<JSX.Element> = rows.map(([info, row]) => {
b69ab31135 return (
b69ab31136 <DagRow
b69ab31137 key={info.hash}
b69ab31138 row={row}
b69ab31139 info={info}
b69ab31140 renderCommit={renderCommit}
b69ab31141 renderCommitExtras={renderCommitExtras}
b69ab31142 renderGlyph={renderGlyph}
b69ab31143 useExtraCommitRowProps={useExtraCommitRowProps}
b69ab31144 />
b69ab31145 );
b69ab31146 });
b69ab31147
b69ab31148 const fullClassName = ((className ?? '') + ' render-dag').trimStart();
b69ab31149 return (
b69ab31150 <div className={fullClassName} {...restProps}>
b69ab31151 <SvgPatternList authors={authors} />
b69ab31152 <AnimatedReorderGroup animationDuration={100}>{renderedRows}</AnimatedReorderGroup>
b69ab31153 </div>
b69ab31154 );
b69ab31155}
b69ab31156
b69ab31157function DivRow(
b69ab31158 props: {
b69ab31159 left?: JSX.Element | null;
b69ab31160 right?: JSX.Element | null;
b69ab31161 } & React.HTMLAttributes<HTMLDivElement> & {['data-commit-hash']?: string},
b69ab31162) {
b69ab31163 const {className, left, right, ...restProps} = props ?? {};
b69ab31164 const fullClassName = `render-dag-row ${className ?? ''}`;
b69ab31165 return (
b69ab31166 <div {...restProps} className={fullClassName}>
b69ab31167 <div className="render-dag-row-left-side">{left}</div>
b69ab31168 <div className="render-dag-row-right-side">{right}</div>
b69ab31169 </div>
b69ab31170 );
b69ab31171}
b69ab31172
b69ab31173function DagRowInner(props: {row: ExtendedGraphRow; info: DagCommitInfo} & RenderFunctionProps) {
b69ab31174 const {
b69ab31175 row,
b69ab31176 info,
b69ab31177 renderGlyph = defaultRenderGlyph,
b69ab31178 renderCommit,
b69ab31179 renderCommitExtras,
b69ab31180 useExtraCommitRowProps,
b69ab31181 } = props;
b69ab31182
b69ab31183 const {className = '', ...commitRowProps} = useExtraCommitRowProps?.(info) ?? {};
b69ab31184
b69ab31185 // Layout per commit:
b69ab31186 //
b69ab31187 // Each (regular) commit is rendered in 2 rows:
b69ab31188 //
b69ab31189 // ┌──Row1──────────────────────────────┐
b69ab31190 // │┌─Left──────────┐┌Right────────────┐│
b69ab31191 // ││┌PreNode*─────┐││ ││
b69ab31192 // │││ | | │││ (commit body) ││
b69ab31193 // ││├Node─────────┤││ ││
b69ab31194 // │││ o | │││ ││
b69ab31195 // ││├PostNode*────┤││ ││
b69ab31196 // │││ | | │││ ││
b69ab31197 // ││└─────────────┘││ ││
b69ab31198 // │└───────────────┘└─────────────────┘│
b69ab31199 // └────────────────────────────────────┘
b69ab31200 //
b69ab31201 // ┌──Row2──────────────────────────────┐
b69ab31202 // │┌─Left──────────┐┌Right────────────┐│
b69ab31203 // ││┌PostNode*────┐││ ││
b69ab31204 // │││ | | │││ ││
b69ab31205 // ││├Term─────────┤││ ││
b69ab31206 // │││ | | │││ (extras) ││
b69ab31207 // │││ | ~ │││ ││
b69ab31208 // ││├Padding──────┤││ ││
b69ab31209 // │││ | │││ ││
b69ab31210 // ││├Link─────────┤││ ││
b69ab31211 // │││ |\ │││ ││
b69ab31212 // │││ | | │││ ││
b69ab31213 // ││├Ancestry─────┤││ ││
b69ab31214 // │││ : | │││ ││
b69ab31215 // │└───────────────┘└─────────────────┘│
b69ab31216 // └────────────────────────────────────┘
b69ab31217 //
b69ab31218 // Note:
b69ab31219 // - Row1 is used to highlight selection. The "node" line should be
b69ab31220 // at the center once selected.
b69ab31221 // - The "*" lines (PreNode, PostNode, PostAncestry) have a stretch
b69ab31222 // height based on the right-side content.
b69ab31223 // - Row2 can be hidden if there is no link line, no ":" ancestry,
b69ab31224 // and no "extras".
b69ab31225 //
b69ab31226 // Example of "You Are here" special case. "Row1" is split to two
b69ab31227 // rows: "Row0" and "Row1":
b69ab31228 //
b69ab31229 // ┌──Row0──────────────────────────────┐
b69ab31230 // │┌─Left─────────────┐ │
b69ab31231 // ││┌Node────────────┐│ │
b69ab31232 // │││ | (YouAreHere) ││ │
b69ab31233 // ││└────────────────┘│ │
b69ab31234 // │└──────────────────┘ │
b69ab31235 // └────────────────────────────────────┘
b69ab31236 // ┌──Row1──────────────────────────────┐
b69ab31237 // │┌─Left──────────┐┌Right────────────┐│
b69ab31238 // ││┌PostNode*────┐││ ││
b69ab31239 // │││ | | │││ (commit body) ││
b69ab31240 // ││└─────────────┘││ ││
b69ab31241 // │└───────────────┘└─────────────────┘│
b69ab31242 // └────────────────────────────────────┘
b69ab31243 //
b69ab31244 // Note:
b69ab31245 // - Row0's "left" side can have a larger width, to fit the
b69ab31246 // "irregular" "(YouAreHere)" element.
b69ab31247 // - Row2 is the same in this special case.
b69ab31248 //
b69ab31249 // Also check fbcode/eden/website/src/components/RenderDag.js
b69ab31250 const {linkLine, termLine, nodeLine, ancestryLine, isHead, isRoot, hasIndirectAncestor} = row;
b69ab31251
b69ab31252 // By default, the glyph "o" is rendered in a fixed size "Tile".
b69ab31253 // With 'replace-tile' the glyph can define its own rendered element
b69ab31254 // (of dynamic size).
b69ab31255 //
b69ab31256 // 'replace-tile' also moves the "commit" element to the right of
b69ab31257 // pad line, not node line.
b69ab31258 const [glyphPosition, glyph] = renderGlyph(info);
b69ab31259 const isIrregular = glyphPosition === 'replace-tile';
b69ab31260 // isYouAreHere practically matches isIrregular but we treat them as
b69ab31261 // separate concepts. isYouAreHere affects colors, and isIrregular
b69ab31262 // affects layout.
b69ab31263 const color = info.isYouAreHere ? YOU_ARE_HERE_COLOR : undefined;
b69ab31264 const nodeLinePart = (
b69ab31265 <div className="render-dag-row-left-side-line node-line">
b69ab31266 {nodeLine.map((l, i) => {
b69ab31267 if (isIrregular && l === NodeLine.Node) {
b69ab31268 return <React.Fragment key={i}>{glyph}</React.Fragment>;
b69ab31269 }
b69ab31270 // Need stretchY if "glyph" is not "Tile" and has a dynamic height.
b69ab31271 return (
b69ab31272 <NodeTile
b69ab31273 key={i}
b69ab31274 line={l}
b69ab31275 isHead={isHead}
b69ab31276 isRoot={isRoot}
b69ab31277 aboveNodeColor={info.isDot ? YOU_ARE_HERE_COLOR : undefined}
b69ab31278 stretchY={isIrregular && l != NodeLine.Node}
b69ab31279 scaleY={isIrregular ? 0.5 : 1}
b69ab31280 glyph={glyph}
b69ab31281 />
b69ab31282 );
b69ab31283 })}
b69ab31284 </div>
b69ab31285 );
b69ab31286
b69ab31287 const preNodeLinePart = (
b69ab31288 <div
b69ab31289 className="render-dag-row-left-side-line pre-node-line grow"
b69ab31290 data-nodecolumn={row.nodeColumn}>
b69ab31291 {row.preNodeLine.map((l, i) => {
b69ab31292 const c = i === row.nodeColumn ? (info.isDot ? YOU_ARE_HERE_COLOR : color) : undefined;
b69ab31293 return <PadTile key={i} line={l} scaleY={0.1} stretchY={true} color={c} />;
b69ab31294 })}
b69ab31295 </div>
b69ab31296 );
b69ab31297
b69ab31298 const postNodeLinePart = (
b69ab31299 <div className="render-dag-row-left-side-line post-node-line grow">
b69ab31300 {row.postNodeLine.map((l, i) => {
b69ab31301 const c = i === row.nodeColumn ? color : undefined;
b69ab31302 return <PadTile key={i} line={l} scaleY={0.1} stretchY={true} color={c} />;
b69ab31303 })}
b69ab31304 </div>
b69ab31305 );
b69ab31306
b69ab31307 const linkLinePart = linkLine && (
b69ab31308 <div className="render-dag-row-left-side-line link-line">
b69ab31309 {linkLine.map((l, i) => (
b69ab31310 <LinkTile key={i} line={l} color={color} colorLine={row.linkLineFromNode?.[i]} />
b69ab31311 ))}
b69ab31312 </div>
b69ab31313 );
b69ab31314
b69ab31315 const stackPaddingPart = linkLine && linkLine.length <= 2 && (
b69ab31316 <div className="render-dag-row-left-side-line stack-padding">
b69ab31317 {linkLine
b69ab31318 // one less than the extra indent added by the link line normally
b69ab31319 .slice(0, -1)
b69ab31320 .map((l, i) => (
b69ab31321 <PadTile key={i} scaleY={0.3} color={color} line={PadLine.Parent} />
b69ab31322 ))}
b69ab31323 </div>
b69ab31324 );
b69ab31325
b69ab31326 const termLinePart = termLine && (
b69ab31327 <>
b69ab31328 <div className="render-dag-row-left-side-line term-line-pad">
b69ab31329 {termLine.map((isTerm, i) => {
b69ab31330 const line = isTerm ? PadLine.Ancestor : (ancestryLine.at(i) ?? PadLine.Blank);
b69ab31331 return <PadTile key={i} scaleY={0.25} line={line} />;
b69ab31332 })}
b69ab31333 </div>
b69ab31334 <div className="render-dag-row-left-side-line term-line-term">
b69ab31335 {termLine.map((isTerm, i) => {
b69ab31336 const line = ancestryLine.at(i) ?? PadLine.Blank;
b69ab31337 return isTerm ? <TermTile key={i} /> : <PadTile key={i} line={line} />;
b69ab31338 })}
b69ab31339 </div>
b69ab31340 </>
b69ab31341 );
b69ab31342
b69ab31343 const commitPart = renderCommit?.(info);
b69ab31344 const commitExtrasPart = renderCommitExtras?.(info, row);
b69ab31345
b69ab31346 const ancestryLinePart = hasIndirectAncestor ? (
b69ab31347 <div className="render-dag-row-left-side-line ancestry-line">
b69ab31348 {ancestryLine.map((l, i) => (
b69ab31349 <PadTile
b69ab31350 key={i}
b69ab31351 scaleY={0.6}
b69ab31352 strokeDashArray="0,2,3,0"
b69ab31353 line={l}
b69ab31354 color={row.parentColumns.includes(i) ? color : undefined}
b69ab31355 />
b69ab31356 ))}
b69ab31357 </div>
b69ab31358 ) : null;
b69ab31359
b69ab31360 // Put parts together.
b69ab31361
b69ab31362 let row0: JSX.Element | null = null;
b69ab31363 let row1: JSX.Element | null = null;
b69ab31364 let row2: JSX.Element | null = null;
b69ab31365 if (isIrregular) {
b69ab31366 row0 = <DivRow className={className} {...commitRowProps} left={nodeLinePart} />;
b69ab31367 row1 = <DivRow left={postNodeLinePart} right={commitPart} />;
b69ab31368 } else {
b69ab31369 const left = (
b69ab31370 <>
b69ab31371 {preNodeLinePart}
b69ab31372 {nodeLinePart}
b69ab31373 {postNodeLinePart}
b69ab31374 </>
b69ab31375 );
b69ab31376 row1 = (
b69ab31377 <DivRow
b69ab31378 className={`render-dag-row-commit ${className ?? ''}`}
b69ab31379 {...commitRowProps}
b69ab31380 left={left}
b69ab31381 right={commitPart}
b69ab31382 data-commit-hash={info.hash}
b69ab31383 />
b69ab31384 );
b69ab31385 }
b69ab31386
b69ab31387 if (
b69ab31388 linkLinePart != null ||
b69ab31389 termLinePart != null ||
b69ab31390 ancestryLinePart != null ||
b69ab31391 postNodeLinePart != null ||
b69ab31392 commitExtrasPart != null
b69ab31393 ) {
b69ab31394 const left = (
b69ab31395 <>
b69ab31396 {commitExtrasPart && postNodeLinePart}
b69ab31397 {linkLinePart}
b69ab31398 {termLinePart}
b69ab31399 {stackPaddingPart}
b69ab31400 {ancestryLinePart}
b69ab31401 </>
b69ab31402 );
b69ab31403 row2 = <DivRow left={left} right={commitExtrasPart} />;
b69ab31404 }
b69ab31405
b69ab31406 return (
b69ab31407 <div
b69ab31408 className="render-dag-row-group"
b69ab31409 data-reorder-id={info.hash}
b69ab31410 data-testid={`dag-row-group-${info.hash}`}>
b69ab31411 {row0}
b69ab31412 {row1}
b69ab31413 {row2}
b69ab31414 </div>
b69ab31415 );
b69ab31416}
b69ab31417
b69ab31418const DagRow = React.memo(DagRowInner, (prevProps, nextProps) => {
b69ab31419 return (
b69ab31420 nextProps.info.equals(prevProps.info) &&
b69ab31421 prevProps.row.valueOf() === nextProps.row.valueOf() &&
b69ab31422 prevProps.renderCommit === nextProps.renderCommit &&
b69ab31423 prevProps.renderCommitExtras === nextProps.renderCommitExtras &&
b69ab31424 prevProps.renderGlyph === nextProps.renderGlyph &&
b69ab31425 prevProps.useExtraCommitRowProps == nextProps.useExtraCommitRowProps
b69ab31426 );
b69ab31427});
b69ab31428
b69ab31429export type TileProps = {
b69ab31430 /** Width. Default: defaultTileWidth. */
b69ab31431 width?: number;
b69ab31432 /** Y scale. Default: 1. Decides height. */
b69ab31433 scaleY?: number;
b69ab31434 /**
b69ab31435 * If true, set:
b69ab31436 * - CSS: height: 100% - take up the height of the (flexbox) parent.
b69ab31437 * - CSS: min-height: width * scaleY, i.e. scaleY affects min-height.
b69ab31438 * - SVG: preserveAspectRatio: 'none'.
b69ab31439 * Intended to be only used by PadLine.
b69ab31440 */
b69ab31441 stretchY?: boolean;
b69ab31442 edges?: Edge[];
b69ab31443 /** SVG children. */
b69ab31444 children?: React.ReactNode;
b69ab31445 /** Line width. Default: strokeWidth. */
b69ab31446 strokeWidth?: number;
b69ab31447 /** Dash array. Default: '3,2'. */
b69ab31448 strokeDashArray?: string;
b69ab31449};
b69ab31450
b69ab31451/**
b69ab31452 * Represent a line within a box (-1,-1) to (1,1).
b69ab31453 * For example, x1=0, y1=-1, x2=0, y2=1 draws a vertical line in the middle.
b69ab31454 * Default x y values are 0.
b69ab31455 * Flag can be used to draw special lines.
b69ab31456 */
b69ab31457export type Edge = {
b69ab31458 x1?: number;
b69ab31459 y1?: number;
b69ab31460 x2?: number;
b69ab31461 y2?: number;
b69ab31462 flag?: number;
b69ab31463 color?: string;
b69ab31464};
b69ab31465
b69ab31466export enum EdgeFlag {
b69ab31467 Dash = 1,
b69ab31468 IntersectGap = 2,
b69ab31469}
b69ab31470
b69ab31471const defaultTileWidth = 20;
b69ab31472const defaultStrokeWidth = 2;
b69ab31473
b69ab31474/**
b69ab31475 * A tile is a rectangle with edges in it.
b69ab31476 * Children are in SVG.
b69ab31477 */
b69ab31478// eslint-disable-next-line prefer-arrow-callback
b69ab31479function TileInner(props: TileProps) {
b69ab31480 const {
b69ab31481 scaleY = 1,
b69ab31482 width = defaultTileWidth,
b69ab31483 edges = [],
b69ab31484 strokeWidth = defaultStrokeWidth,
b69ab31485 strokeDashArray = '3,2',
b69ab31486 stretchY = false,
b69ab31487 } = props;
b69ab31488 const preserveAspectRatio = stretchY || scaleY < 1 ? 'none' : undefined;
b69ab31489 const height = width * scaleY;
b69ab31490 const style = stretchY ? {height: '100%', minHeight: height} : {};
b69ab31491 // Fill the small caused by scaling, non-integer rounding.
b69ab31492 // When 'x' is at the border (abs >= 10) and 'y' is at the center, use the "gap fix".
b69ab31493 const getGapFix = (x: number, y: number) =>
b69ab31494 y === 0 && Math.abs(x) >= 10 ? 0.5 * Math.sign(x) : 0;
b69ab31495 const paths = edges.map(({x1 = 0, y1 = 0, x2 = 0, y2 = 0, flag = 0, color}, i): JSX.Element => {
b69ab31496 // see getGapFix above.
b69ab31497 const fx1 = getGapFix(x1, y1);
b69ab31498 const fx2 = getGapFix(x2, y2);
b69ab31499 const fy1 = getGapFix(y1, x1);
b69ab31500 const fy2 = getGapFix(y2, x2);
b69ab31501
b69ab31502 const sY = scaleY;
b69ab31503 const dashArray = flag & EdgeFlag.Dash ? strokeDashArray : undefined;
b69ab31504 let d;
b69ab31505 if (flag & EdgeFlag.IntersectGap) {
b69ab31506 // This vertical line intercects with a horizontal line visually but it does not mean
b69ab31507 // they connect. Leave a small gap in the middle.
b69ab31508 d = `M ${x1 + fx1} ${y1 * sY + fy1} L 0 -2 M 0 2 L ${x2 + fx2} ${y2 * sY + fy2}`;
b69ab31509 } else if (y1 === y2 || x1 === x2) {
b69ab31510 // Straight line (-----).
b69ab31511 d = `M ${x1 + fx1} ${y1 * sY + fy1} L ${x2 + fx2} ${y2 * sY + fy2}`;
b69ab31512 } else {
b69ab31513 // Curved line (towards center).
b69ab31514 d = `M ${x1 + fx1} ${y1 * sY + fy1} L ${x1} ${y1 * sY} Q 0 0 ${x2} ${y2 * sY} L ${x2 + fx2} ${
b69ab31515 y2 * sY + fy2
b69ab31516 }`;
b69ab31517 }
b69ab31518 return <path d={d} key={i} strokeDasharray={dashArray} stroke={color} />;
b69ab31519 });
b69ab31520 return (
b69ab31521 <svg
b69ab31522 className="render-dag-tile"
b69ab31523 viewBox={`-10 -${scaleY * 10} 20 ${scaleY * 20}`}
b69ab31524 height={height}
b69ab31525 width={width}
b69ab31526 style={style}
b69ab31527 preserveAspectRatio={preserveAspectRatio}>
b69ab31528 <g stroke="var(--foreground)" fill="none" strokeWidth={strokeWidth}>
b69ab31529 {paths}
b69ab31530 {props.children}
b69ab31531 </g>
b69ab31532 </svg>
b69ab31533 );
b69ab31534}
b69ab31535const Tile = React.memo(TileInner);
b69ab31536
b69ab31537function NodeTile(
b69ab31538 props: {
b69ab31539 line: NodeLine;
b69ab31540 isHead: boolean;
b69ab31541 isRoot: boolean;
b69ab31542 glyph: JSX.Element;
b69ab31543 /** For NodeLine.Node, the color of the vertical edge above the circle. */
b69ab31544 aboveNodeColor?: string;
b69ab31545 } & TileProps,
b69ab31546) {
b69ab31547 const {line, isHead, isRoot, glyph} = props;
b69ab31548 switch (line) {
b69ab31549 case NodeLine.Ancestor:
b69ab31550 return <Tile {...props} edges={[{y1: -10, y2: 10, flag: EdgeFlag.Dash}]} />;
b69ab31551 case NodeLine.Parent:
b69ab31552 // 10.5 is used instead of 10 to avoid small gaps when the page is zoomed.
b69ab31553 return <Tile {...props} edges={[{y1: -10, y2: 10.5}]} />;
b69ab31554 case NodeLine.Node: {
b69ab31555 const edges: Edge[] = [];
b69ab31556 if (!isHead) {
b69ab31557 edges.push({y1: -10.5, color: props.aboveNodeColor});
b69ab31558 }
b69ab31559 if (!isRoot) {
b69ab31560 edges.push({y2: 10.5});
b69ab31561 }
b69ab31562 return (
b69ab31563 <Tile {...props} edges={edges}>
b69ab31564 {glyph}
b69ab31565 </Tile>
b69ab31566 );
b69ab31567 }
b69ab31568 default:
b69ab31569 return <Tile {...props} edges={[]} />;
b69ab31570 }
b69ab31571}
b69ab31572
b69ab31573function PadTile(props: {line: PadLine; color?: string} & TileProps) {
b69ab31574 const {line, color} = props;
b69ab31575 switch (line) {
b69ab31576 case PadLine.Ancestor:
b69ab31577 return <Tile {...props} edges={[{y1: -10, y2: 10, flag: EdgeFlag.Dash, color}]} />;
b69ab31578 case PadLine.Parent:
b69ab31579 return <Tile {...props} edges={[{y1: -10, y2: 10, color}]} />;
b69ab31580 default:
b69ab31581 return <Tile {...props} edges={[]} />;
b69ab31582 }
b69ab31583}
b69ab31584
b69ab31585function TermTile(props: TileProps) {
b69ab31586 // "~" in svg.
b69ab31587 return (
b69ab31588 <Tile {...props}>
b69ab31589 <path d="M 0 -10 L 0 -5" strokeDasharray="3,2" />
b69ab31590 <path d="M -7 -5 Q -3 -8, 0 -5 T 7 -5" />
b69ab31591 </Tile>
b69ab31592 );
b69ab31593}
b69ab31594
b69ab31595function LinkTile(props: {line: LinkLine; color?: string; colorLine?: LinkLine} & TileProps) {
b69ab31596 const edges = linkLineToEdges(props.line, props.color, props.colorLine);
b69ab31597 return <Tile {...props} edges={edges} />;
b69ab31598}
b69ab31599
b69ab31600function linkLineToEdges(linkLine: LinkLine, color?: string, colorLine?: LinkLine): Edge[] {
b69ab31601 const bits = linkLine.valueOf();
b69ab31602 const colorBits = colorLine?.valueOf() ?? 0;
b69ab31603 const edges: Edge[] = [];
b69ab31604 const considerEdge = (parentBits: number, ancestorBits: number, edge: Partial<Edge>) => {
b69ab31605 const present = (bits & (parentBits | ancestorBits)) !== 0;
b69ab31606 const useColor = (colorBits & (parentBits | ancestorBits)) !== 0;
b69ab31607 const dashed = (bits & ancestorBits) !== 0;
b69ab31608 if (present) {
b69ab31609 const flag = edge.flag ?? 0 | (dashed ? EdgeFlag.Dash : 0);
b69ab31610 edges.push({...edge, flag, color: useColor ? color : undefined});
b69ab31611 }
b69ab31612 };
b69ab31613 considerEdge(LinkLine.VERT_PARENT, LinkLine.VERT_ANCESTOR, {
b69ab31614 y1: -10,
b69ab31615 y2: 10,
b69ab31616 flag: bits & (LinkLine.HORIZ_PARENT | LinkLine.HORIZ_ANCESTOR) ? EdgeFlag.IntersectGap : 0,
b69ab31617 });
b69ab31618 considerEdge(LinkLine.HORIZ_PARENT, LinkLine.HORIZ_ANCESTOR, {x1: -10, x2: 10});
b69ab31619 considerEdge(LinkLine.LEFT_MERGE_PARENT, LinkLine.LEFT_MERGE_ANCESTOR, {x1: -10, y2: -10});
b69ab31620 considerEdge(LinkLine.RIGHT_MERGE_PARENT, LinkLine.RIGHT_MERGE_ANCESTOR, {x1: 10, y2: -10});
b69ab31621 considerEdge(LinkLine.LEFT_FORK_PARENT | LinkLine.LEFT_FORK_ANCESTOR, 0, {x1: -10, y2: 10});
b69ab31622 considerEdge(LinkLine.RIGHT_FORK_PARENT | LinkLine.RIGHT_FORK_ANCESTOR, 0, {x1: 10, y2: 10});
b69ab31623 return edges;
b69ab31624}
b69ab31625
b69ab31626// Svg patterns for avatar backgrounds. Those patterns are referred later by `RegularGlyph`.
b69ab31627function SvgPatternList(props: {authors: Iterable<string>}) {
b69ab31628 return (
b69ab31629 <svg className="render-dag-svg-patterns" viewBox={`-10 -10 20 20`}>
b69ab31630 <defs>
b69ab31631 {[...props.authors].map(author => (
b69ab31632 <SvgPattern author={author} key={author} />
b69ab31633 ))}
b69ab31634 </defs>
b69ab31635 </svg>
b69ab31636 );
b69ab31637}
b69ab31638
b69ab31639function authorToSvgPatternId(author: string) {
b69ab31640 return 'avatar-pattern-' + author.replace(/[^A-Z0-9a-z]/g, '_');
b69ab31641}
b69ab31642
b69ab31643function SvgPatternInner(props: {author: string}) {
b69ab31644 const {author} = props;
b69ab31645 const id = authorToSvgPatternId(author);
b69ab31646 return (
b69ab31647 <AvatarPattern
b69ab31648 size={DEFAULT_GLYPH_RADIUS * 2}
b69ab31649 username={author}
b69ab31650 id={id}
b69ab31651 fallbackFill="var(--foreground)"
b69ab31652 />
b69ab31653 );
b69ab31654}
b69ab31655
b69ab31656const SvgPattern = React.memo(SvgPatternInner);
b69ab31657
ab83ad3658const YOU_ARE_HERE_COLOR = '#4d8a78';
ab83ad3659const DEFAULT_GLYPH_RADIUS = (defaultTileWidth * 3.5) / 20;
b69ab31660
b69ab31661function RegularGlyphInner({info}: {info: DagCommitInfo}) {
b69ab31662 const stroke = info.isDot ? YOU_ARE_HERE_COLOR : 'var(--foreground)';
b69ab31663 const r = DEFAULT_GLYPH_RADIUS;
b69ab31664 const strokeWidth = defaultStrokeWidth * 0.9;
b69ab31665 const isObsoleted = info.successorInfo != null;
b69ab31666 let fill = 'var(--foreground)';
b69ab31667 let extraSvgElement = null;
b69ab31668 if (info.phase === 'draft') {
b69ab31669 if (isObsoleted) {
b69ab31670 // "/" inside the circle (similar to "x" in CLI) to indicate "obsoleted".
b69ab31671 fill = 'var(--background)';
b69ab31672 const pos = r / Math.sqrt(2) - strokeWidth;
b69ab31673 extraSvgElement = (
b69ab31674 <path
b69ab31675 d={`M ${-pos} ${pos} L ${pos} ${-pos}`}
b69ab31676 stroke={stroke}
b69ab31677 strokeWidth={strokeWidth}
b69ab31678 strokeLinecap="round"
b69ab31679 />
b69ab31680 );
b69ab31681 } else if (info.author.length > 0) {
b69ab31682 // Avatar for draft, non-obsoleted commits.
b69ab31683 const id = authorToSvgPatternId(info.author);
b69ab31684 fill = `url(#${id})`;
b69ab31685 }
b69ab31686 }
b69ab31687
ab83ad3688 if (info.isDot && !isObsoleted) {
ab83ad3689 return (
ab83ad3690 <>
ab83ad3691 <circle cx={0} cy={0} r={r + strokeWidth * 2.5} fill={'var(--background)'} stroke="none" />
ab83ad3692 <circle cx={0} cy={0} r={r + strokeWidth * 2} fill="none" stroke={YOU_ARE_HERE_COLOR} strokeWidth={strokeWidth} />
e6f6bcb693 <circle cx={0} cy={0} r={r} fill={YOU_ARE_HERE_COLOR} stroke="none" />
ab83ad3694 </>
ab83ad3695 );
ab83ad3696 }
ab83ad3697
b69ab31698 return (
b69ab31699 <>
b69ab31700 <circle cx={0} cy={0} r={r} fill={fill} stroke={stroke} strokeWidth={strokeWidth} />
b69ab31701 {extraSvgElement}
b69ab31702 </>
b69ab31703 );
b69ab31704}
b69ab31705
b69ab31706export const RegularGlyph = React.memo(RegularGlyphInner, (prevProps, nextProps) => {
b69ab31707 const prevInfo = prevProps.info;
b69ab31708 const nextInfo = nextProps.info;
b69ab31709 return nextInfo.equals(prevInfo);
b69ab31710});
b69ab31711
b69ab31712/**
b69ab31713 * The default "You are here" glyph - render as a blue bubble. Intended to be used in
b69ab31714 * different `RenderDag` configurations.
b69ab31715 *
b69ab31716 * If you want to customize the rendering for the main graph, or introducing dependencies
b69ab31717 * that seem "extra" (like code review states, operation-related progress state), consider
b69ab31718 * passing the `renderGlyph` prop to `RenderDag` instead. See `CommitTreeList` for example.
b69ab31719 */
b69ab31720export function YouAreHereGlyph({info, children}: {info: DagCommitInfo; children?: ReactNode}) {
b69ab31721 return (
b69ab31722 <YouAreHereLabel title={info.description} style={{marginLeft: -defaultStrokeWidth * 1.5}}>
b69ab31723 {children}
b69ab31724 </YouAreHereLabel>
b69ab31725 );
b69ab31726}
b69ab31727
b69ab31728export function defaultRenderGlyph(info: DagCommitInfo): RenderGlyphResult {
b69ab31729 if (info.isYouAreHere) {
b69ab31730 return ['replace-tile', <YouAreHereGlyph info={info} />];
b69ab31731 } else {
b69ab31732 return ['inside-tile', <RegularGlyph info={info} />];
b69ab31733 }
b69ab31734}