diff --git a/README.md b/README.md
index e2ffd8a..fb1b49c 100644
--- a/README.md
+++ b/README.md
@@ -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 = () => (
+
+
+
+);
+
+Renderer.render();
+```
+
**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)
diff --git a/src/components/Svg/RNSvg.ts b/src/components/Svg/RNSvg.ts
new file mode 100644
index 0000000..c26e60f
--- /dev/null
+++ b/src/components/Svg/RNSvg.ts
@@ -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, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+
+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 ``;
+ }
+}
+
+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 });
+ }
+}
diff --git a/src/components/Svg/index.ts b/src/components/Svg/index.ts
new file mode 100644
index 0000000..1449440
--- /dev/null
+++ b/src/components/Svg/index.ts
@@ -0,0 +1,112 @@
+import { WidgetEventTypes } from "@nodegui/nodegui";
+import { Fiber } from "react-reconciler";
+import { AppContainer } from "../../reconciler";
+import { ComponentConfig, RNProps, registerComponent } from "../config";
+import {
+ RNSvg,
+ RNSvgCircle,
+ RNSvgEllipse,
+ RNSvgGroup,
+ RNSvgLine,
+ RNSvgNode,
+ RNSvgPath,
+ RNSvgPolygon,
+ RNSvgRect,
+ SvgCircleProps,
+ SvgEllipseProps,
+ SvgLineProps,
+ SvgNodeProps,
+ SvgPathProps,
+ SvgPolygonProps,
+ SvgProps,
+ SvgRectProps,
+} from "./RNSvg";
+
+type SvgComponentFactory = new () => RNSvgNode;
+
+class SvgConfig extends ComponentConfig {
+ tagName = RNSvg.tagName;
+ shouldSetTextContent(): boolean {
+ return false;
+ }
+ createInstance(
+ newProps: SvgProps,
+ rootInstance: AppContainer,
+ context: any,
+ workInProgress: Fiber
+ ): RNSvg {
+ const widget = new RNSvg();
+ widget.setProperty("scaledContents", true);
+ widget.setProps(newProps, {});
+ widget.addEventListener(WidgetEventTypes.Resize, () => {
+ widget.scalePixmap(widget.size());
+ });
+ return widget;
+ }
+ commitMount(instance: RNSvg, newProps: SvgProps): void {
+ if (newProps.visible !== false) {
+ instance.show();
+ }
+ }
+ commitUpdate(
+ instance: RNSvg,
+ updatePayload: any,
+ oldProps: SvgProps,
+ newProps: SvgProps,
+ finishedWork: Fiber
+ ): void {
+ instance.setProps(newProps, oldProps);
+ }
+}
+
+class SvgNodeConfig extends ComponentConfig {
+ constructor(public tagName: string, private NodeClass: SvgComponentFactory) {
+ super();
+ }
+ shouldSetTextContent(): boolean {
+ return false;
+ }
+ createInstance(
+ newProps: T,
+ rootInstance: AppContainer,
+ context: any,
+ workInProgress: Fiber
+ ): RNSvgNode {
+ const node = new this.NodeClass();
+ node.setProps(newProps as SvgNodeProps, {});
+ return node;
+ }
+ commitUpdate(
+ instance: RNSvgNode,
+ updatePayload: any,
+ oldProps: T,
+ newProps: T,
+ finishedWork: Fiber
+ ): void {
+ instance.setProps(newProps as SvgNodeProps, oldProps as SvgNodeProps);
+ }
+}
+
+export const Svg = registerComponent(new SvgConfig());
+export const G = registerComponent(
+ new SvgNodeConfig(RNSvgGroup.tagName, RNSvgGroup)
+);
+export const Group = G;
+export const Rect = registerComponent(
+ new SvgNodeConfig(RNSvgRect.tagName, RNSvgRect)
+);
+export const Circle = registerComponent(
+ new SvgNodeConfig(RNSvgCircle.tagName, RNSvgCircle)
+);
+export const Ellipse = registerComponent(
+ new SvgNodeConfig(RNSvgEllipse.tagName, RNSvgEllipse)
+);
+export const Line = registerComponent(
+ new SvgNodeConfig(RNSvgLine.tagName, RNSvgLine)
+);
+export const Polygon = registerComponent(
+ new SvgNodeConfig(RNSvgPolygon.tagName, RNSvgPolygon)
+);
+export const Path = registerComponent(
+ new SvgNodeConfig(RNSvgPath.tagName, RNSvgPath)
+);
diff --git a/src/index.ts b/src/index.ts
index 78e18b8..c5ad56a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,6 +9,17 @@ export { Window } from "./components/Window";
export { Text } from "./components/Text";
export { Image } from "./components/Image";
export { AnimatedImage } from "./components/AnimatedImage";
+export {
+ Svg,
+ G,
+ Group,
+ Rect,
+ Circle,
+ Ellipse,
+ Line,
+ Polygon,
+ Path,
+} from "./components/Svg";
export { Button } from "./components/Button";
export { CheckBox } from "./components/CheckBox";
export { LineEdit } from "./components/LineEdit";