25.0 KB735 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9import type {Dag, DagCommitInfo} from './dag/dag';
10import type {ExtendedGraphRow} from './dag/render';
11import type {HashSet} from './dag/set';
12
13import React from 'react';
14import {AnimatedReorderGroup} from './AnimatedReorderGroup';
15import {AvatarPattern} from './Avatar';
16import {YouAreHereLabel} from './YouAreHereLabel';
17import {LinkLine, NodeLine, PadLine} from './dag/render';
18
19import './RenderDag.css';
20
21/* eslint no-bitwise: 0 */
22
23export type RenderDagProps = {
24 /** The dag to use */
25 dag: Dag;
26
27 /** If set, render a subset. Otherwise, all commits are rendered. */
28 subset?: HashSet;
29
30 /** Should "anonymous" parents (rendered as "~" in CLI) be ignored? */
31 ignoreAnonymousParents?: boolean;
32} & React.HTMLAttributes<HTMLDivElement> &
33 RenderFunctionProps;
34
35type RenderFunctionProps = {
36 /**
37 * How to render a commit.
38 *
39 * To avoid re-rendering, pass a "static" (ex. not a closure) function,
40 * then use hooks (ex. recoil selector) to trigger re-rendering inside
41 * the static function.
42 */
43 renderCommit?: (info: DagCommitInfo) => JSX.Element;
44
45 /**
46 * How to render extra stuff below a commit. Default: nothing.
47 *
48 * To avoid re-rendering, pass a "static" (ex. not a closure) function,
49 * then use hooks (ex. recoil selector) to trigger re-rendering inside
50 * the static function.
51 */
52 renderCommitExtras?: (info: DagCommitInfo, row: ExtendedGraphRow) => null | JSX.Element;
53
54 /**
55 * How to render a "glyph" (ex. "o", "x", "@").
56 * This should return an SVG element.
57 * The SVG viewbox is (-10,-10) to (10,10) (20px * 20px).
58 * Default: defaultRenderGlyphSvg, draw a circle.
59 *
60 * To avoid re-rendering, pass a "static" (ex. not a closure) function,
61 * then use hooks (ex. recoil selector) to trigger re-rendering inside
62 * the static function.
63 */
64 renderGlyph?: (info: DagCommitInfo) => RenderGlyphResult;
65
66 /**
67 * Get extra props for the DivRow for the given commit.
68 * This can be used to tweak styles like selection background, border.
69 * This should be a static-ish function to avoid re-rendering. Inside the function,
70 * it can use hooks to fetch extra state.
71 */
72 useExtraCommitRowProps?: (info: DagCommitInfo) => React.HTMLAttributes<HTMLDivElement> | void;
73};
74
75/**
76 * - 'inside-tile': Inside a <Tile />. Must be a svg element. Size decided by <Tile />.
77 * - 'replace-tile': Replace the <Tile /> with the rendered result. Size decided by the
78 * rendered result. Can be other elements not just svg. Useful for "You are here".
79 */
80export type RenderGlyphResult = ['inside-tile', JSX.Element] | ['replace-tile', JSX.Element];
81
82/**
83 * Renders a dag. Calculate and render the edges, aka. the left side:
84 *
85 * o +--------+
86 * | | commit |
87 * | +--------+
88 * |
89 * | o +--------+
90 * |/ | commit |
91 * o +--------+
92 * :\
93 * : o +--------+
94 * : | commit |
95 * : +--------+
96 * :
97 * o +--------+
98 * | commit |
99 * +--------+
100 *
101 * The callsite can customize:
102 * - What "dag" and what subset of commits to render.
103 * - How to render each "commit" (the boxes above).
104 * - How to render the glyph (the "o").
105 *
106 * For a commit with `info.isYouAreHere` set, the "commit" body
107 * will be positioned at the right of the "pad" line, not the
108 * "node" line, and the default "o" rendering logic will render
109 * a blue badge instead.
110 *
111 * See `DagListProps` for customization options.
112 *
113 * This component is intended to be used in multiple places,
114 * ex. the main dag, "mutation dag", context-menu sub-dag, etc.
115 * So it should avoid depending on Recoil states.
116 */
117export function RenderDag(props: RenderDagProps) {
118 const {
119 dag,
120 subset,
121 renderCommit,
122 renderCommitExtras,
123 renderGlyph = defaultRenderGlyph,
124 useExtraCommitRowProps,
125 className,
126 ...restProps
127 } = props;
128
129 const rows = dag.renderToRows(subset);
130 const authors = new Set<string>(
131 rows.flatMap(([info]) => (info.phase === 'draft' && info.author.length > 0 ? info.author : [])),
132 );
133
134 const renderedRows: Array<JSX.Element> = rows.map(([info, row]) => {
135 return (
136 <DagRow
137 key={info.hash}
138 row={row}
139 info={info}
140 renderCommit={renderCommit}
141 renderCommitExtras={renderCommitExtras}
142 renderGlyph={renderGlyph}
143 useExtraCommitRowProps={useExtraCommitRowProps}
144 />
145 );
146 });
147
148 const fullClassName = ((className ?? '') + ' render-dag').trimStart();
149 return (
150 <div className={fullClassName} {...restProps}>
151 <SvgPatternList authors={authors} />
152 <AnimatedReorderGroup animationDuration={100}>{renderedRows}</AnimatedReorderGroup>
153 </div>
154 );
155}
156
157function DivRow(
158 props: {
159 left?: JSX.Element | null;
160 right?: JSX.Element | null;
161 } & React.HTMLAttributes<HTMLDivElement> & {['data-commit-hash']?: string},
162) {
163 const {className, left, right, ...restProps} = props ?? {};
164 const fullClassName = `render-dag-row ${className ?? ''}`;
165 return (
166 <div {...restProps} className={fullClassName}>
167 <div className="render-dag-row-left-side">{left}</div>
168 <div className="render-dag-row-right-side">{right}</div>
169 </div>
170 );
171}
172
173function DagRowInner(props: {row: ExtendedGraphRow; info: DagCommitInfo} & RenderFunctionProps) {
174 const {
175 row,
176 info,
177 renderGlyph = defaultRenderGlyph,
178 renderCommit,
179 renderCommitExtras,
180 useExtraCommitRowProps,
181 } = props;
182
183 const {className = '', ...commitRowProps} = useExtraCommitRowProps?.(info) ?? {};
184
185 // Layout per commit:
186 //
187 // Each (regular) commit is rendered in 2 rows:
188 //
189 // ┌──Row1──────────────────────────────┐
190 // │┌─Left──────────┐┌Right────────────┐│
191 // ││┌PreNode*─────┐││ ││
192 // │││ | | │││ (commit body) ││
193 // ││├Node─────────┤││ ││
194 // │││ o | │││ ││
195 // ││├PostNode*────┤││ ││
196 // │││ | | │││ ││
197 // ││└─────────────┘││ ││
198 // │└───────────────┘└─────────────────┘│
199 // └────────────────────────────────────┘
200 //
201 // ┌──Row2──────────────────────────────┐
202 // │┌─Left──────────┐┌Right────────────┐│
203 // ││┌PostNode*────┐││ ││
204 // │││ | | │││ ││
205 // ││├Term─────────┤││ ││
206 // │││ | | │││ (extras) ││
207 // │││ | ~ │││ ││
208 // ││├Padding──────┤││ ││
209 // │││ | │││ ││
210 // ││├Link─────────┤││ ││
211 // │││ |\ │││ ││
212 // │││ | | │││ ││
213 // ││├Ancestry─────┤││ ││
214 // │││ : | │││ ││
215 // │└───────────────┘└─────────────────┘│
216 // └────────────────────────────────────┘
217 //
218 // Note:
219 // - Row1 is used to highlight selection. The "node" line should be
220 // at the center once selected.
221 // - The "*" lines (PreNode, PostNode, PostAncestry) have a stretch
222 // height based on the right-side content.
223 // - Row2 can be hidden if there is no link line, no ":" ancestry,
224 // and no "extras".
225 //
226 // Example of "You Are here" special case. "Row1" is split to two
227 // rows: "Row0" and "Row1":
228 //
229 // ┌──Row0──────────────────────────────┐
230 // │┌─Left─────────────┐ │
231 // ││┌Node────────────┐│ │
232 // │││ | (YouAreHere) ││ │
233 // ││└────────────────┘│ │
234 // │└──────────────────┘ │
235 // └────────────────────────────────────┘
236 // ┌──Row1──────────────────────────────┐
237 // │┌─Left──────────┐┌Right────────────┐│
238 // ││┌PostNode*────┐││ ││
239 // │││ | | │││ (commit body) ││
240 // ││└─────────────┘││ ││
241 // │└───────────────┘└─────────────────┘│
242 // └────────────────────────────────────┘
243 //
244 // Note:
245 // - Row0's "left" side can have a larger width, to fit the
246 // "irregular" "(YouAreHere)" element.
247 // - Row2 is the same in this special case.
248 //
249 // Also check fbcode/eden/website/src/components/RenderDag.js
250 const {linkLine, termLine, nodeLine, ancestryLine, isHead, isRoot, hasIndirectAncestor} = row;
251
252 // By default, the glyph "o" is rendered in a fixed size "Tile".
253 // With 'replace-tile' the glyph can define its own rendered element
254 // (of dynamic size).
255 //
256 // 'replace-tile' also moves the "commit" element to the right of
257 // pad line, not node line.
258 const [glyphPosition, glyph] = renderGlyph(info);
259 const isIrregular = glyphPosition === 'replace-tile';
260 // isYouAreHere practically matches isIrregular but we treat them as
261 // separate concepts. isYouAreHere affects colors, and isIrregular
262 // affects layout.
263 const color = info.isYouAreHere ? YOU_ARE_HERE_COLOR : undefined;
264 const nodeLinePart = (
265 <div className="render-dag-row-left-side-line node-line">
266 {nodeLine.map((l, i) => {
267 if (isIrregular && l === NodeLine.Node) {
268 return <React.Fragment key={i}>{glyph}</React.Fragment>;
269 }
270 // Need stretchY if "glyph" is not "Tile" and has a dynamic height.
271 return (
272 <NodeTile
273 key={i}
274 line={l}
275 isHead={isHead}
276 isRoot={isRoot}
277 aboveNodeColor={info.isDot ? YOU_ARE_HERE_COLOR : undefined}
278 stretchY={isIrregular && l != NodeLine.Node}
279 scaleY={isIrregular ? 0.5 : 1}
280 glyph={glyph}
281 />
282 );
283 })}
284 </div>
285 );
286
287 const preNodeLinePart = (
288 <div
289 className="render-dag-row-left-side-line pre-node-line grow"
290 data-nodecolumn={row.nodeColumn}>
291 {row.preNodeLine.map((l, i) => {
292 const c = i === row.nodeColumn ? (info.isDot ? YOU_ARE_HERE_COLOR : color) : undefined;
293 return <PadTile key={i} line={l} scaleY={0.1} stretchY={true} color={c} />;
294 })}
295 </div>
296 );
297
298 const postNodeLinePart = (
299 <div className="render-dag-row-left-side-line post-node-line grow">
300 {row.postNodeLine.map((l, i) => {
301 const c = i === row.nodeColumn ? color : undefined;
302 return <PadTile key={i} line={l} scaleY={0.1} stretchY={true} color={c} />;
303 })}
304 </div>
305 );
306
307 const linkLinePart = linkLine && (
308 <div className="render-dag-row-left-side-line link-line">
309 {linkLine.map((l, i) => (
310 <LinkTile key={i} line={l} color={color} colorLine={row.linkLineFromNode?.[i]} />
311 ))}
312 </div>
313 );
314
315 const stackPaddingPart = linkLine && linkLine.length <= 2 && (
316 <div className="render-dag-row-left-side-line stack-padding">
317 {linkLine
318 // one less than the extra indent added by the link line normally
319 .slice(0, -1)
320 .map((l, i) => (
321 <PadTile key={i} scaleY={0.3} color={color} line={PadLine.Parent} />
322 ))}
323 </div>
324 );
325
326 const termLinePart = termLine && (
327 <>
328 <div className="render-dag-row-left-side-line term-line-pad">
329 {termLine.map((isTerm, i) => {
330 const line = isTerm ? PadLine.Ancestor : (ancestryLine.at(i) ?? PadLine.Blank);
331 return <PadTile key={i} scaleY={0.25} line={line} />;
332 })}
333 </div>
334 <div className="render-dag-row-left-side-line term-line-term">
335 {termLine.map((isTerm, i) => {
336 const line = ancestryLine.at(i) ?? PadLine.Blank;
337 return isTerm ? <TermTile key={i} /> : <PadTile key={i} line={line} />;
338 })}
339 </div>
340 </>
341 );
342
343 const commitPart = renderCommit?.(info);
344 const commitExtrasPart = renderCommitExtras?.(info, row);
345
346 const ancestryLinePart = hasIndirectAncestor ? (
347 <div className="render-dag-row-left-side-line ancestry-line">
348 {ancestryLine.map((l, i) => (
349 <PadTile
350 key={i}
351 scaleY={0.6}
352 strokeDashArray="0,2,3,0"
353 line={l}
354 color={row.parentColumns.includes(i) ? color : undefined}
355 />
356 ))}
357 </div>
358 ) : null;
359
360 // Put parts together.
361
362 let row0: JSX.Element | null = null;
363 let row1: JSX.Element | null = null;
364 let row2: JSX.Element | null = null;
365 if (isIrregular) {
366 row0 = <DivRow className={className} {...commitRowProps} left={nodeLinePart} />;
367 row1 = <DivRow left={postNodeLinePart} right={commitPart} />;
368 } else {
369 const left = (
370 <>
371 {preNodeLinePart}
372 {nodeLinePart}
373 {postNodeLinePart}
374 </>
375 );
376 row1 = (
377 <DivRow
378 className={`render-dag-row-commit ${className ?? ''}`}
379 {...commitRowProps}
380 left={left}
381 right={commitPart}
382 data-commit-hash={info.hash}
383 />
384 );
385 }
386
387 if (
388 linkLinePart != null ||
389 termLinePart != null ||
390 ancestryLinePart != null ||
391 postNodeLinePart != null ||
392 commitExtrasPart != null
393 ) {
394 const left = (
395 <>
396 {commitExtrasPart && postNodeLinePart}
397 {linkLinePart}
398 {termLinePart}
399 {stackPaddingPart}
400 {ancestryLinePart}
401 </>
402 );
403 row2 = <DivRow left={left} right={commitExtrasPart} />;
404 }
405
406 return (
407 <div
408 className="render-dag-row-group"
409 data-reorder-id={info.hash}
410 data-testid={`dag-row-group-${info.hash}`}>
411 {row0}
412 {row1}
413 {row2}
414 </div>
415 );
416}
417
418const DagRow = React.memo(DagRowInner, (prevProps, nextProps) => {
419 return (
420 nextProps.info.equals(prevProps.info) &&
421 prevProps.row.valueOf() === nextProps.row.valueOf() &&
422 prevProps.renderCommit === nextProps.renderCommit &&
423 prevProps.renderCommitExtras === nextProps.renderCommitExtras &&
424 prevProps.renderGlyph === nextProps.renderGlyph &&
425 prevProps.useExtraCommitRowProps == nextProps.useExtraCommitRowProps
426 );
427});
428
429export type TileProps = {
430 /** Width. Default: defaultTileWidth. */
431 width?: number;
432 /** Y scale. Default: 1. Decides height. */
433 scaleY?: number;
434 /**
435 * If true, set:
436 * - CSS: height: 100% - take up the height of the (flexbox) parent.
437 * - CSS: min-height: width * scaleY, i.e. scaleY affects min-height.
438 * - SVG: preserveAspectRatio: 'none'.
439 * Intended to be only used by PadLine.
440 */
441 stretchY?: boolean;
442 edges?: Edge[];
443 /** SVG children. */
444 children?: React.ReactNode;
445 /** Line width. Default: strokeWidth. */
446 strokeWidth?: number;
447 /** Dash array. Default: '3,2'. */
448 strokeDashArray?: string;
449};
450
451/**
452 * Represent a line within a box (-1,-1) to (1,1).
453 * For example, x1=0, y1=-1, x2=0, y2=1 draws a vertical line in the middle.
454 * Default x y values are 0.
455 * Flag can be used to draw special lines.
456 */
457export type Edge = {
458 x1?: number;
459 y1?: number;
460 x2?: number;
461 y2?: number;
462 flag?: number;
463 color?: string;
464};
465
466export enum EdgeFlag {
467 Dash = 1,
468 IntersectGap = 2,
469}
470
471const defaultTileWidth = 20;
472const defaultStrokeWidth = 2;
473
474/**
475 * A tile is a rectangle with edges in it.
476 * Children are in SVG.
477 */
478// eslint-disable-next-line prefer-arrow-callback
479function TileInner(props: TileProps) {
480 const {
481 scaleY = 1,
482 width = defaultTileWidth,
483 edges = [],
484 strokeWidth = defaultStrokeWidth,
485 strokeDashArray = '3,2',
486 stretchY = false,
487 } = props;
488 const preserveAspectRatio = stretchY || scaleY < 1 ? 'none' : undefined;
489 const height = width * scaleY;
490 const style = stretchY ? {height: '100%', minHeight: height} : {};
491 // Fill the small caused by scaling, non-integer rounding.
492 // When 'x' is at the border (abs >= 10) and 'y' is at the center, use the "gap fix".
493 const getGapFix = (x: number, y: number) =>
494 y === 0 && Math.abs(x) >= 10 ? 0.5 * Math.sign(x) : 0;
495 const paths = edges.map(({x1 = 0, y1 = 0, x2 = 0, y2 = 0, flag = 0, color}, i): JSX.Element => {
496 // see getGapFix above.
497 const fx1 = getGapFix(x1, y1);
498 const fx2 = getGapFix(x2, y2);
499 const fy1 = getGapFix(y1, x1);
500 const fy2 = getGapFix(y2, x2);
501
502 const sY = scaleY;
503 const dashArray = flag & EdgeFlag.Dash ? strokeDashArray : undefined;
504 let d;
505 if (flag & EdgeFlag.IntersectGap) {
506 // This vertical line intercects with a horizontal line visually but it does not mean
507 // they connect. Leave a small gap in the middle.
508 d = `M ${x1 + fx1} ${y1 * sY + fy1} L 0 -2 M 0 2 L ${x2 + fx2} ${y2 * sY + fy2}`;
509 } else if (y1 === y2 || x1 === x2) {
510 // Straight line (-----).
511 d = `M ${x1 + fx1} ${y1 * sY + fy1} L ${x2 + fx2} ${y2 * sY + fy2}`;
512 } else {
513 // Curved line (towards center).
514 d = `M ${x1 + fx1} ${y1 * sY + fy1} L ${x1} ${y1 * sY} Q 0 0 ${x2} ${y2 * sY} L ${x2 + fx2} ${
515 y2 * sY + fy2
516 }`;
517 }
518 return <path d={d} key={i} strokeDasharray={dashArray} stroke={color} />;
519 });
520 return (
521 <svg
522 className="render-dag-tile"
523 viewBox={`-10 -${scaleY * 10} 20 ${scaleY * 20}`}
524 height={height}
525 width={width}
526 style={style}
527 preserveAspectRatio={preserveAspectRatio}>
528 <g stroke="var(--foreground)" fill="none" strokeWidth={strokeWidth}>
529 {paths}
530 {props.children}
531 </g>
532 </svg>
533 );
534}
535const Tile = React.memo(TileInner);
536
537function NodeTile(
538 props: {
539 line: NodeLine;
540 isHead: boolean;
541 isRoot: boolean;
542 glyph: JSX.Element;
543 /** For NodeLine.Node, the color of the vertical edge above the circle. */
544 aboveNodeColor?: string;
545 } & TileProps,
546) {
547 const {line, isHead, isRoot, glyph} = props;
548 switch (line) {
549 case NodeLine.Ancestor:
550 return <Tile {...props} edges={[{y1: -10, y2: 10, flag: EdgeFlag.Dash}]} />;
551 case NodeLine.Parent:
552 // 10.5 is used instead of 10 to avoid small gaps when the page is zoomed.
553 return <Tile {...props} edges={[{y1: -10, y2: 10.5}]} />;
554 case NodeLine.Node: {
555 const edges: Edge[] = [];
556 if (!isHead) {
557 edges.push({y1: -10.5, color: props.aboveNodeColor});
558 }
559 if (!isRoot) {
560 edges.push({y2: 10.5});
561 }
562 return (
563 <Tile {...props} edges={edges}>
564 {glyph}
565 </Tile>
566 );
567 }
568 default:
569 return <Tile {...props} edges={[]} />;
570 }
571}
572
573function PadTile(props: {line: PadLine; color?: string} & TileProps) {
574 const {line, color} = props;
575 switch (line) {
576 case PadLine.Ancestor:
577 return <Tile {...props} edges={[{y1: -10, y2: 10, flag: EdgeFlag.Dash, color}]} />;
578 case PadLine.Parent:
579 return <Tile {...props} edges={[{y1: -10, y2: 10, color}]} />;
580 default:
581 return <Tile {...props} edges={[]} />;
582 }
583}
584
585function TermTile(props: TileProps) {
586 // "~" in svg.
587 return (
588 <Tile {...props}>
589 <path d="M 0 -10 L 0 -5" strokeDasharray="3,2" />
590 <path d="M -7 -5 Q -3 -8, 0 -5 T 7 -5" />
591 </Tile>
592 );
593}
594
595function LinkTile(props: {line: LinkLine; color?: string; colorLine?: LinkLine} & TileProps) {
596 const edges = linkLineToEdges(props.line, props.color, props.colorLine);
597 return <Tile {...props} edges={edges} />;
598}
599
600function linkLineToEdges(linkLine: LinkLine, color?: string, colorLine?: LinkLine): Edge[] {
601 const bits = linkLine.valueOf();
602 const colorBits = colorLine?.valueOf() ?? 0;
603 const edges: Edge[] = [];
604 const considerEdge = (parentBits: number, ancestorBits: number, edge: Partial<Edge>) => {
605 const present = (bits & (parentBits | ancestorBits)) !== 0;
606 const useColor = (colorBits & (parentBits | ancestorBits)) !== 0;
607 const dashed = (bits & ancestorBits) !== 0;
608 if (present) {
609 const flag = edge.flag ?? 0 | (dashed ? EdgeFlag.Dash : 0);
610 edges.push({...edge, flag, color: useColor ? color : undefined});
611 }
612 };
613 considerEdge(LinkLine.VERT_PARENT, LinkLine.VERT_ANCESTOR, {
614 y1: -10,
615 y2: 10,
616 flag: bits & (LinkLine.HORIZ_PARENT | LinkLine.HORIZ_ANCESTOR) ? EdgeFlag.IntersectGap : 0,
617 });
618 considerEdge(LinkLine.HORIZ_PARENT, LinkLine.HORIZ_ANCESTOR, {x1: -10, x2: 10});
619 considerEdge(LinkLine.LEFT_MERGE_PARENT, LinkLine.LEFT_MERGE_ANCESTOR, {x1: -10, y2: -10});
620 considerEdge(LinkLine.RIGHT_MERGE_PARENT, LinkLine.RIGHT_MERGE_ANCESTOR, {x1: 10, y2: -10});
621 considerEdge(LinkLine.LEFT_FORK_PARENT | LinkLine.LEFT_FORK_ANCESTOR, 0, {x1: -10, y2: 10});
622 considerEdge(LinkLine.RIGHT_FORK_PARENT | LinkLine.RIGHT_FORK_ANCESTOR, 0, {x1: 10, y2: 10});
623 return edges;
624}
625
626// Svg patterns for avatar backgrounds. Those patterns are referred later by `RegularGlyph`.
627function SvgPatternList(props: {authors: Iterable<string>}) {
628 return (
629 <svg className="render-dag-svg-patterns" viewBox={`-10 -10 20 20`}>
630 <defs>
631 {[...props.authors].map(author => (
632 <SvgPattern author={author} key={author} />
633 ))}
634 </defs>
635 </svg>
636 );
637}
638
639function authorToSvgPatternId(author: string) {
640 return 'avatar-pattern-' + author.replace(/[^A-Z0-9a-z]/g, '_');
641}
642
643function SvgPatternInner(props: {author: string}) {
644 const {author} = props;
645 const id = authorToSvgPatternId(author);
646 return (
647 <AvatarPattern
648 size={DEFAULT_GLYPH_RADIUS * 2}
649 username={author}
650 id={id}
651 fallbackFill="var(--foreground)"
652 />
653 );
654}
655
656const SvgPattern = React.memo(SvgPatternInner);
657
658const YOU_ARE_HERE_COLOR = '#4d8a78';
659const DEFAULT_GLYPH_RADIUS = (defaultTileWidth * 3.5) / 20;
660
661function RegularGlyphInner({info}: {info: DagCommitInfo}) {
662 const stroke = info.isDot ? YOU_ARE_HERE_COLOR : 'var(--foreground)';
663 const r = DEFAULT_GLYPH_RADIUS;
664 const strokeWidth = defaultStrokeWidth * 0.9;
665 const isObsoleted = info.successorInfo != null;
666 let fill = 'var(--foreground)';
667 let extraSvgElement = null;
668 if (info.phase === 'draft') {
669 if (isObsoleted) {
670 // "/" inside the circle (similar to "x" in CLI) to indicate "obsoleted".
671 fill = 'var(--background)';
672 const pos = r / Math.sqrt(2) - strokeWidth;
673 extraSvgElement = (
674 <path
675 d={`M ${-pos} ${pos} L ${pos} ${-pos}`}
676 stroke={stroke}
677 strokeWidth={strokeWidth}
678 strokeLinecap="round"
679 />
680 );
681 } else if (info.author.length > 0) {
682 // Avatar for draft, non-obsoleted commits.
683 const id = authorToSvgPatternId(info.author);
684 fill = `url(#${id})`;
685 }
686 }
687
688 if (info.isDot && !isObsoleted) {
689 return (
690 <>
691 <circle cx={0} cy={0} r={r + strokeWidth * 2.5} fill={'var(--background)'} stroke="none" />
692 <circle cx={0} cy={0} r={r + strokeWidth * 2} fill="none" stroke={YOU_ARE_HERE_COLOR} strokeWidth={strokeWidth} />
693 <circle cx={0} cy={0} r={r} fill={YOU_ARE_HERE_COLOR} stroke="none" />
694 </>
695 );
696 }
697
698 return (
699 <>
700 <circle cx={0} cy={0} r={r} fill={fill} stroke={stroke} strokeWidth={strokeWidth} />
701 {extraSvgElement}
702 </>
703 );
704}
705
706export const RegularGlyph = React.memo(RegularGlyphInner, (prevProps, nextProps) => {
707 const prevInfo = prevProps.info;
708 const nextInfo = nextProps.info;
709 return nextInfo.equals(prevInfo);
710});
711
712/**
713 * The default "You are here" glyph - render as a blue bubble. Intended to be used in
714 * different `RenderDag` configurations.
715 *
716 * If you want to customize the rendering for the main graph, or introducing dependencies
717 * that seem "extra" (like code review states, operation-related progress state), consider
718 * passing the `renderGlyph` prop to `RenderDag` instead. See `CommitTreeList` for example.
719 */
720export function YouAreHereGlyph({info, children}: {info: DagCommitInfo; children?: ReactNode}) {
721 return (
722 <YouAreHereLabel title={info.description} style={{marginLeft: -defaultStrokeWidth * 1.5}}>
723 {children}
724 </YouAreHereLabel>
725 );
726}
727
728export function defaultRenderGlyph(info: DagCommitInfo): RenderGlyphResult {
729 if (info.isYouAreHere) {
730 return ['replace-tile', <YouAreHereGlyph info={info} />];
731 } else {
732 return ['inside-tile', <RegularGlyph info={info} />];
733 }
734}
735