Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,31 @@ Visit: https://react.nodegui.org for docs.
- Read through the [docs](https://react.nodegui.org)

**Community Guides**

- https://gregbenner.life/node-gui-react-component-by-component/ - An awesome intro to all base components in react nodegui

- https://blog.logrocket.com/electron-alternatives-exploring-nodegui-and-react-nodegui/ - Electron alternatives: Exploring NodeGUI and React NodeGUI by [Siegfried Grimbeek](https://blog.logrocket.com/author/siegfriedgrimbeek/).

## SVG

React NodeGUI supports rendering simple SVG trees by serializing SVG-like React components and rendering them as an image.

```jsx
import React from "react";
import { Renderer, Svg, Rect, Circle, Window } from "@nodegui/react-nodegui";

const App = () => (
<Window>
<Svg width={160} height={120} viewBox="0 0 160 120">
<Rect x={10} y={10} width={140} height={100} fill="#20242a" rx={12} />
<Circle cx={80} cy={60} r={32} fill="#61dafb" />
</Svg>
</Window>
);

Renderer.render(<App />);
```

**Talks/Podcasts**

- [NodeGui and React NodeGui at KarmaJS Nov 2019 meetup: https://www.youtube.com/watch?v=8jH5gaEEDv4](https://www.youtube.com/watch?v=8jH5gaEEDv4)
Expand Down
331 changes: 331 additions & 0 deletions src/components/Svg/RNSvg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
import { QLabel, QPixmap, QSize } from "@nodegui/nodegui";
import { Component } from "@nodegui/nodegui/dist/lib/core/Component";
import { RNComponent, RNProps } from "../config";
import { TextProps, setTextProps } from "../Text/RNText";

export type SvgValue = string | number | boolean | undefined | null;

export interface SvgProps extends TextProps {
width?: number | string;
height?: number | string;
viewBox?: string;
preserveAspectRatio?: string;
xmlns?: string;
fill?: SvgValue;
stroke?: SvgValue;
strokeWidth?: SvgValue;
style?: string;
}

export interface SvgNodeProps extends RNProps {
fill?: SvgValue;
stroke?: SvgValue;
strokeWidth?: SvgValue;
opacity?: SvgValue;
transform?: string;
style?: string;
id?: string;
className?: string;
}

export interface SvgRectProps extends SvgNodeProps {
x?: SvgValue;
y?: SvgValue;
width?: SvgValue;
height?: SvgValue;
rx?: SvgValue;
ry?: SvgValue;
}

export interface SvgCircleProps extends SvgNodeProps {
cx?: SvgValue;
cy?: SvgValue;
r?: SvgValue;
}

export interface SvgEllipseProps extends SvgNodeProps {
cx?: SvgValue;
cy?: SvgValue;
rx?: SvgValue;
ry?: SvgValue;
}

export interface SvgLineProps extends SvgNodeProps {
x1?: SvgValue;
y1?: SvgValue;
x2?: SvgValue;
y2?: SvgValue;
}

export interface SvgPolygonProps extends SvgNodeProps {
points?: string;
}

export interface SvgPathProps extends SvgNodeProps {
d?: string;
}

const svgNamespace = "http://www.w3.org/2000/svg";

const svgAttributeMap: { [key: string]: string } = {
className: "class",
preserveAspectRatio: "preserveAspectRatio",
strokeWidth: "stroke-width",
};

const escapeXml = (value: SvgValue): string =>
String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");

const serializeAttributes = (props: { [key: string]: SvgValue }): string =>
Object.keys(props)
.filter(
(key) =>
props[key] !== undefined && props[key] !== null && props[key] !== false
)
.map((key) => {
const attributeName = svgAttributeMap[key] || key;
return `${attributeName}="${escapeXml(props[key])}"`;
})
.join(" ");

const serializeElement = (
tagName: string,
props: { [key: string]: SvgValue },
children: RNSvgNode[] = []
): string => {
const attributes = serializeAttributes(props);
const openTag = attributes ? `<${tagName} ${attributes}` : `<${tagName}`;
if (!children.length) {
return `${openTag} />`;
}
return `${openTag}>${children
.map((child) => child.toSvgString())
.join("")}</${tagName}>`;
};

const isSvgNode = (child: Component): child is RNSvgNode =>
child instanceof RNSvgNode;

export abstract class RNSvgNode extends Component implements RNComponent {
static tagName: string;
protected props: SvgNodeProps = {};
protected svgChildren: RNSvgNode[] = [];
private parent: RNSvgNode | RNSvg | null = null;

constructor() {
super({ type: "native" });
}

setProps(newProps: SvgNodeProps, oldProps: SvgNodeProps): void {
this.props = newProps;
this.requestRender();
}

appendInitialChild(child: Component): void {
this.appendChild(child);
}

appendChild(child: Component): void {
if (!isSvgNode(child)) {
return;
}
this.svgChildren.push(child);
child.setSvgParent(this);
this.requestRender();
}

insertBefore(child: Component, beforeChild: Component): void {
if (!isSvgNode(child) || !isSvgNode(beforeChild)) {
return;
}
const index = this.svgChildren.indexOf(beforeChild);
if (index === -1) {
this.appendChild(child);
return;
}
this.svgChildren.splice(index, 0, child);
child.setSvgParent(this);
this.requestRender();
}

removeChild(child: Component): void {
if (!isSvgNode(child)) {
return;
}
this.svgChildren = this.svgChildren.filter(
(existingChild) => existingChild !== child
);
child.setSvgParent(null);
this.requestRender();
}

setSvgParent(parent: RNSvgNode | RNSvg | null): void {
this.parent = parent;
}

requestRender(): void {
if (this.parent) {
this.parent.requestRender();
}
}

abstract toSvgString(): string;
}

export class RNSvg extends QLabel implements RNComponent {
static tagName = "svg";
private props: SvgProps = {};
private svgChildren: RNSvgNode[] = [];

setProps(newProps: SvgProps, oldProps: SvgProps): void {
this.props = newProps;
setTextProps(this as any, newProps, oldProps);
this.renderSvg();
}

appendInitialChild(child: Component): void {
this.appendChild(child);
}

appendChild(child: Component): void {
if (!isSvgNode(child)) {
return;
}
this.svgChildren.push(child);
child.setSvgParent(this);
this.renderSvg();
}

insertBefore(child: Component, beforeChild: Component): void {
if (!isSvgNode(child) || !isSvgNode(beforeChild)) {
return;
}
const index = this.svgChildren.indexOf(beforeChild);
if (index === -1) {
this.appendChild(child);
return;
}
this.svgChildren.splice(index, 0, child);
child.setSvgParent(this);
this.renderSvg();
}

removeChild(child: Component): void {
if (!isSvgNode(child)) {
return;
}
this.svgChildren = this.svgChildren.filter(
(existingChild) => existingChild !== child
);
child.setSvgParent(null);
this.renderSvg();
}

requestRender(): void {
this.renderSvg();
}

scalePixmap(size: QSize): void {
this.renderSvg(size.width(), size.height());
}

private renderSvg(
width = this.size().width(),
height = this.size().height()
): void {
const pixmap = new QPixmap();
pixmap.loadFromData(Buffer.from(this.toSvgString(width, height)));
this.setPixmap(pixmap);
}

private toSvgString(renderWidth: number, renderHeight: number): string {
const width = this.props.width || renderWidth || 100;
const height = this.props.height || renderHeight || 100;
const attributes = serializeAttributes({
fill: this.props.fill,
height,
preserveAspectRatio: this.props.preserveAspectRatio,
stroke: this.props.stroke,
strokeWidth: this.props.strokeWidth,
style: this.props.style,
viewBox: this.props.viewBox,
width,
xmlns: this.props.xmlns || svgNamespace,
});
return `<svg ${attributes}>${this.svgChildren
.map((child) => child.toSvgString())
.join("")}</svg>`;
}
}

export class RNSvgGroup extends RNSvgNode {
static tagName = "g";
toSvgString(): string {
return serializeElement(
"g",
this.props as { [key: string]: SvgValue },
this.svgChildren
);
}
}

export class RNSvgRect extends RNSvgNode {
static tagName = "rect";
protected props: SvgRectProps = {};
toSvgString(): string {
return serializeElement("rect", this.props as { [key: string]: SvgValue });
}
}

export class RNSvgCircle extends RNSvgNode {
static tagName = "circle";
protected props: SvgCircleProps = {};
toSvgString(): string {
return serializeElement(
"circle",
this.props as { [key: string]: SvgValue }
);
}
}

export class RNSvgEllipse extends RNSvgNode {
static tagName = "ellipse";
protected props: SvgEllipseProps = {};
toSvgString(): string {
return serializeElement(
"ellipse",
this.props as { [key: string]: SvgValue }
);
}
}

export class RNSvgLine extends RNSvgNode {
static tagName = "line";
protected props: SvgLineProps = {};
toSvgString(): string {
return serializeElement("line", this.props as { [key: string]: SvgValue });
}
}

export class RNSvgPolygon extends RNSvgNode {
static tagName = "polygon";
protected props: SvgPolygonProps = {};
toSvgString(): string {
return serializeElement(
"polygon",
this.props as { [key: string]: SvgValue }
);
}
}

export class RNSvgPath extends RNSvgNode {
static tagName = "path";
protected props: SvgPathProps = {};
toSvgString(): string {
return serializeElement("path", this.props as { [key: string]: SvgValue });
}
}
Loading