diff --git a/bindings/bindings.nim b/bindings/bindings.nim new file mode 100644 index 0000000..39e54e4 --- /dev/null +++ b/bindings/bindings.nim @@ -0,0 +1,587 @@ +import genny +import std/tables +import std/hashes +import figdraw/commons +import figdraw/fignodes as fdn +import figdraw/common/fonttypes as fnt +import figdraw/common/fontutils as fut + +const ExportSiwinShim* {.booldefine: "figdraw.bindings.siwinshim".} = false + +type + FigKind* = fdn.FigKind + + RgbaColor* = object + r*: uint8 + g*: uint8 + b*: uint8 + a*: uint8 + + Fig* = ref object + inner: fdn.Fig + + RenderList* = ref object + inner: fdn.RenderList + + Renders* = ref object + inner: fdn.Renders + + TypefaceRef* = ref object + id: fnt.TypefaceId + + FigFontRef* = ref object + inner: fnt.FigFont + + GlyphLayoutRef* = ref object + inner: fnt.GlyphArrangement + +proc newFig(): Fig = + Fig(inner: fdn.Fig(kind: fdn.nkFrame)) + +proc newRgbaColor(r, g, b, a: uint8): RgbaColor = + RgbaColor(r: r, g: g, b: b, a: a) + +proc newRectangleFig(x, y, w, h: float32): Fig = + Fig( + inner: fdn.Fig( + kind: fdn.nkRectangle, + screenBox: rect(x, y, w, h), + fill: fill(rgba(255, 255, 255, 255)), + ), + ) + +proc newTextFig(x, y, w, h: float32): Fig = + Fig( + inner: fdn.Fig( + kind: fdn.nkText, + screenBox: rect(x, y, w, h), + fill: fill(rgba(255, 255, 255, 255)), + textLayout: GlyphArrangement(), + ), + ) + +proc newImageFig(x, y, w, h: float32, imageId: int64): Fig = + Fig( + inner: fdn.Fig( + kind: fdn.nkImage, + screenBox: rect(x, y, w, h), + fill: fill(rgba(255, 255, 255, 255)), + image: ImageStyle( + id: cast[ImageId](Hash(imageId)), + fill: fill(rgba(255, 255, 255, 255)), + ), + ), + ) + +proc newTransformFig(x, y, w, h: float32, tx, ty: float32): Fig = + Fig( + inner: fdn.Fig( + kind: fdn.nkTransform, + screenBox: rect(x, y, w, h), + fill: fill(rgba(255, 255, 255, 255)), + transform: TransformStyle( + translation: vec2(tx, ty), + useMatrix: false, + ), + ), + ) + +proc loadTypefaceBinding(name: string): TypefaceRef = + try: + let fontId = fut.loadTypeface(name) + TypefaceRef(id: cast[fnt.TypefaceId](fontId)) + except CatchableError: + nil + +proc newFigFontBinding(typeface: TypefaceRef, size: float32): FigFontRef = + if typeface.isNil: + return nil + FigFontRef(inner: fnt.FigFont(typefaceId: typeface.id, size: size)) + +proc setFigFontLineHeightBinding(font: FigFontRef, lineHeight: float32) = + if font.isNil: + return + font.inner.lineHeight = lineHeight + +proc setFigFontCaseBinding(font: FigFontRef, fontCase: int8) = + if font.isNil: + return + case fontCase + of 1'i8: + font.inner.fontCase = fnt.FontCase.UpperCase + of 2'i8: + font.inner.fontCase = fnt.FontCase.LowerCase + of 3'i8: + font.inner.fontCase = fnt.FontCase.TitleCase + else: + font.inner.fontCase = fnt.FontCase.NormalCase + +proc typesetTextBinding( + width, height: float32, + font: FigFontRef, + text: string, + hAlign: int8 = 0, + vAlign: int8 = 0, + minContent = false, + wrap = false, +): GlyphLayoutRef = + if font.isNil: + return nil + var h = fnt.FontHorizontal.Left + case hAlign + of 1'i8: + h = fnt.FontHorizontal.Center + of 2'i8: + h = fnt.FontHorizontal.Right + else: + discard + + var v = fnt.FontVertical.Top + case vAlign + of 1'i8: + v = fnt.FontVertical.Middle + of 2'i8: + v = fnt.FontVertical.Bottom + else: + discard + + try: + let layout = fut.typeset( + box = rect(0'f32, 0'f32, width, height), + uiSpans = @[(font.inner, text)], + hAlign = h, + vAlign = v, + minContent = minContent, + wrap = wrap, + ) + GlyphLayoutRef(inner: layout) + except CatchableError: + nil + +proc setFigTextLayoutBinding(fig: Fig, layout: GlyphLayoutRef) = + if fig.isNil or layout.isNil: + return + if fig.inner.kind != fdn.nkText: + fig.inner.kind = fdn.nkText + fig.inner.textLayout = layout.inner + +proc textLayoutWidthBinding(layout: GlyphLayoutRef): float32 = + if layout.isNil: + return 0'f32 + layout.inner.bounding.w + +proc textLayoutHeightBinding(layout: GlyphLayoutRef): float32 = + if layout.isNil: + return 0'f32 + layout.inner.bounding.h + +proc copy(fig: Fig): Fig = + Fig(inner: fig.inner) + +proc figWithKind(src: fdn.Fig, kind: FigKind): fdn.Fig = + result = fdn.Fig(kind: fdn.FigKind(kind)) + result.zlevel = src.zlevel + result.parent = src.parent + result.flags = src.flags + result.childCount = src.childCount + result.screenBox = src.screenBox + result.rotation = src.rotation + result.fill = src.fill + result.corners = src.corners + +proc kind(fig: Fig): FigKind = + fig.inner.kind + +proc setKind(fig: Fig, kind: FigKind) = + fig.inner = figWithKind(fig.inner, kind) + +proc zLevel(fig: Fig): int8 = + fig.inner.zlevel.int8 + +proc setZLevel(fig: Fig, zLevel: int8) = + fig.inner.zlevel = fdn.ZLevel(zLevel) + +proc x(fig: Fig): float32 = + fig.inner.screenBox.x + +proc y(fig: Fig): float32 = + fig.inner.screenBox.y + +proc width(fig: Fig): float32 = + fig.inner.screenBox.w + +proc height(fig: Fig): float32 = + fig.inner.screenBox.h + +proc setScreenBox(fig: Fig, x, y, w, h: float32) = + fig.inner.screenBox = rect(x, y, w, h) + +proc setFillColor(fig: Fig, r, g, b, a: uint8) = + fig.inner.fill = fill(rgba(r, g, b, a)) + +proc setFillColorRgba(fig: Fig, color: RgbaColor) = + fig.inner.fill = fill(rgba(color.r, color.g, color.b, color.a)) + +proc parseFillAxis(axis: int8): FillGradientAxis = + case axis + of 1'i8: + fgaY + of 2'i8: + fgaDiagTLBR + of 3'i8: + fgaDiagBLTR + else: + fgaX + +proc setFillLinear2( + fig: Fig, + sr, sg, sb, sa: uint8, + er, eg, eb, ea: uint8, + axis: int8, +) = + fig.inner.fill = linear( + rgba(sr, sg, sb, sa), + rgba(er, eg, eb, ea), + axis = parseFillAxis(axis), + ) + +proc setFillLinear2Rgba( + fig: Fig, + startColor, endColor: RgbaColor, + axis: int8, +) = + fig.inner.fill = linear( + rgba(startColor.r, startColor.g, startColor.b, startColor.a), + rgba(endColor.r, endColor.g, endColor.b, endColor.a), + axis = parseFillAxis(axis), + ) + +proc setFillLinear3( + fig: Fig, + sr, sg, sb, sa: uint8, + mr, mg, mb, ma: uint8, + er, eg, eb, ea: uint8, + axis: int8, + midPos: uint8, +) = + fig.inner.fill = linear( + rgba(sr, sg, sb, sa), + rgba(mr, mg, mb, ma), + rgba(er, eg, eb, ea), + axis = parseFillAxis(axis), + midPos = midPos, + ) + +proc setFillLinear3Rgba( + fig: Fig, + startColor, midColor, endColor: RgbaColor, + axis: int8, + midPos: uint8, +) = + fig.inner.fill = linear( + rgba(startColor.r, startColor.g, startColor.b, startColor.a), + rgba(midColor.r, midColor.g, midColor.b, midColor.a), + rgba(endColor.r, endColor.g, endColor.b, endColor.a), + axis = parseFillAxis(axis), + midPos = midPos, + ) + +proc setRotation(fig: Fig, rotation: float32) = + fig.inner.rotation = rotation + +proc setCorners(fig: Fig, topLeft, topRight, bottomLeft, bottomRight: float32) = + fig.inner.corners = [topLeft, topRight, bottomLeft, bottomRight] + +proc setStroke(fig: Fig, weight: float32, r, g, b, a: uint8) = + if fig.inner.kind != fdn.nkRectangle: + fig.inner = figWithKind(fig.inner, fdn.nkRectangle) + fig.inner.stroke = RenderStroke( + weight: weight, + fill: fill(rgba(r, g, b, a)), + ) + +proc setStrokeRgba(fig: Fig, weight: float32, color: RgbaColor) = + if fig.inner.kind != fdn.nkRectangle: + fig.inner = figWithKind(fig.inner, fdn.nkRectangle) + fig.inner.stroke = RenderStroke( + weight: weight, + fill: fill(rgba(color.r, color.g, color.b, color.a)), + ) + +proc clearShadows(fig: Fig) = + if fig.inner.kind != fdn.nkRectangle: + fig.inner = figWithKind(fig.inner, fdn.nkRectangle) + fig.inner.shadows = [RenderShadow(), RenderShadow(), RenderShadow(), RenderShadow()] + +proc setShadow( + fig: Fig, + shadowIndex: int8, + style: int8, + blur, spread, x, y: float32, + r, g, b, a: uint8, +) = + if fig.inner.kind != fdn.nkRectangle: + fig.inner = figWithKind(fig.inner, fdn.nkRectangle) + if shadowIndex < 0'i8 or shadowIndex >= ShadowCount.int8: + return + + var shadowStyle = ShadowStyle.NoShadow + case style + of 1'i8: + shadowStyle = ShadowStyle.DropShadow + of 2'i8: + shadowStyle = ShadowStyle.InnerShadow + else: + discard + + fig.inner.shadows[shadowIndex.int] = RenderShadow( + style: shadowStyle, + blur: blur, + spread: spread, + x: x, + y: y, + fill: fill(rgba(r, g, b, a)), + ) + +proc setShadowRgba( + fig: Fig, + shadowIndex: int8, + style: int8, + blur, spread, x, y: float32, + color: RgbaColor, +) = + if fig.inner.kind != fdn.nkRectangle: + fig.inner = figWithKind(fig.inner, fdn.nkRectangle) + if shadowIndex < 0'i8 or shadowIndex >= ShadowCount.int8: + return + + var shadowStyle = ShadowStyle.NoShadow + case style + of 1'i8: + shadowStyle = ShadowStyle.DropShadow + of 2'i8: + shadowStyle = ShadowStyle.InnerShadow + else: + discard + + fig.inner.shadows[shadowIndex.int] = RenderShadow( + style: shadowStyle, + blur: blur, + spread: spread, + x: x, + y: y, + fill: fill(rgba(color.r, color.g, color.b, color.a)), + ) + +proc newRenderList(): RenderList = + RenderList(inner: fdn.RenderList()) + +proc copy(list: RenderList): RenderList = + RenderList(inner: list.inner) + +proc clear(list: RenderList) = + list.inner = fdn.RenderList() + +proc nodeCount(list: RenderList): int = + list.inner.nodes.len + +proc rootCount(list: RenderList): int = + list.inner.rootIds.len + +proc addRoot(list: RenderList, root: Fig): int16 = + list.inner.addRoot(root.inner).int16 + +proc addChild(list: RenderList, parentIdx: int16, child: Fig): int16 = + try: + list.inner.addChild(fdn.FigIdx(parentIdx), child.inner).int16 + except ValueError: + -1'i16 + +proc getNode(list: RenderList, nodeIdx: int16): Fig = + Fig(inner: list.inner.nodes[nodeIdx.int]) + +proc getRootId(list: RenderList, rootIdx: int16): int16 = + list.inner.rootIds[rootIdx.int].int16 + +proc newRenders(): Renders = + Renders(inner: fdn.Renders(layers: initOrderedTable[fdn.ZLevel, fdn.RenderList]())) + +proc clear(renders: Renders) = + renders.inner.layers.clear() + +proc containsLayer(renders: Renders, zLevel: int8): bool = + renders.inner.contains(fdn.ZLevel(zLevel)) + +proc addRoot(renders: Renders, zLevel: int8, root: Fig): int16 = + try: + renders.inner.addRoot(fdn.ZLevel(zLevel), root.inner).int16 + except CatchableError: + -1'i16 + +proc addChild(renders: Renders, zLevel: int8, parentIdx: int16, child: Fig): int16 = + try: + renders.inner.addChild( + fdn.ZLevel(zLevel), + fdn.FigIdx(parentIdx), + child.inner, + ).int16 + except CatchableError: + -1'i16 + +proc layerNodeCount(renders: Renders, zLevel: int8): int = + try: + if not renders.containsLayer(zLevel): + return 0 + renders.inner[fdn.ZLevel(zLevel)].nodes.len + except CatchableError: + 0 + +proc layerRootCount(renders: Renders, zLevel: int8): int = + try: + if not renders.containsLayer(zLevel): + return 0 + renders.inner[fdn.ZLevel(zLevel)].rootIds.len + except CatchableError: + 0 + +proc getLayerNode(renders: Renders, zLevel: int8, nodeIdx: int16): Fig = + try: + Fig(inner: renders.inner[fdn.ZLevel(zLevel)].nodes[nodeIdx.int]) + except CatchableError: + newFig() + +exportEnums: + FigKind + +exportObject RgbaColor: + constructor: + newRgbaColor(uint8, uint8, uint8, uint8) + +exportRefObject Fig: + constructor: + newFig() + procs: + copy(Fig) + kind(Fig) + setKind(Fig, FigKind) + zLevel(Fig) + setZLevel(Fig, int8) + x(Fig) + y(Fig) + width(Fig) + height(Fig) + setScreenBox(Fig, float32, float32, float32, float32) + setFillColor(Fig, uint8, uint8, uint8, uint8) + setFillColorRgba(Fig, RgbaColor) + setFillLinear2( + Fig, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + int8, + ) + setFillLinear2Rgba(Fig, RgbaColor, RgbaColor, int8) + setFillLinear3( + Fig, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + uint8, + int8, + uint8, + ) + setFillLinear3Rgba(Fig, RgbaColor, RgbaColor, RgbaColor, int8, uint8) + setRotation(Fig, float32) + setCorners(Fig, float32, float32, float32, float32) + setStroke(Fig, float32, uint8, uint8, uint8, uint8) + setStrokeRgba(Fig, float32, RgbaColor) + clearShadows(Fig) + setShadow( + Fig, + int8, + int8, + float32, + float32, + float32, + float32, + uint8, + uint8, + uint8, + uint8, + ) + setShadowRgba(Fig, int8, int8, float32, float32, float32, float32, RgbaColor) + +exportRefObject RenderList: + constructor: + newRenderList() + procs: + copy(RenderList) + clear(RenderList) + nodeCount(RenderList) + rootCount(RenderList) + addRoot(RenderList, Fig) + addChild(RenderList, int16, Fig) + getNode(RenderList, int16) + getRootId(RenderList, int16) + +exportRefObject Renders: + constructor: + newRenders() + procs: + clear(Renders) + containsLayer(Renders, int8) + addRoot(Renders, int8, Fig) + addChild(Renders, int8, int16, Fig) + layerNodeCount(Renders, int8) + layerRootCount(Renders, int8) + getLayerNode(Renders, int8, int16) + +exportRefObject TypefaceRef: + discard + +exportRefObject FigFontRef: + constructor: + newFigFontBinding(TypefaceRef, float32) + procs: + setFigFontLineHeightBinding(FigFontRef, float32) + setFigFontCaseBinding(FigFontRef, int8) + +exportRefObject GlyphLayoutRef: + procs: + textLayoutWidthBinding(GlyphLayoutRef) + textLayoutHeightBinding(GlyphLayoutRef) + +exportProcs: + newRectangleFig + newTextFig + newImageFig + newTransformFig + loadTypefaceBinding + typesetTextBinding + setFigTextLayoutBinding + figDataDir + setFigDataDir + figUiScale + setFigUiScale + scaled(float32) + descaled(float32) + +when ExportSiwinShim: + include siwinshim_bindings + +writeFiles("bindings/generated", "FigDraw") + +include generated/internal diff --git a/bindings/siwinshim_bindings.nim b/bindings/siwinshim_bindings.nim new file mode 100644 index 0000000..5f93cd3 --- /dev/null +++ b/bindings/siwinshim_bindings.nim @@ -0,0 +1,356 @@ +import figdraw/windowing/siwinshim +import figdraw/figrender as fgr +when defined(macosx) and UseMetalBackend: + import figdraw/windowing/siwinmetal + +type + SiwinWindowRef* = ref object + inner: Window + + SiwinRendererRef* = ref object + inner: fgr.FigRenderer[SiwinRenderBackend] + +when defined(macosx) and UseMetalBackend: + type + SiwinMetalLayerRef* = ref object + inner: MetalLayerHandle + +var + siwinBackendNameStorage {.threadvar.}: string + siwinWindowTitleStorage {.threadvar.}: string + siwinDisplayNameStorage {.threadvar.}: string + siwinRendererBackendNameStorage {.threadvar.}: string + siwinRendererWindowTitleStorage {.threadvar.}: string + +proc siwinBackendNameBinding(): string = + try: + siwinBackendNameStorage = siwinBackendName() + siwinBackendNameStorage + except Exception: + "" + +proc siwinWindowTitleBinding(suffix: string): string = + try: + siwinWindowTitleStorage = siwinWindowTitle(suffix = suffix) + siwinWindowTitleStorage + except Exception: + "" + +proc sharedSiwinGlobalsPtrBinding(): uint64 = + try: + cast[uint64](sharedSiwinGlobals()) + except Exception: + 0'u64 + +proc newSiwinRendererBinding(atlasSize: int, pixelScale: float32): SiwinRendererRef = + try: + SiwinRendererRef( + inner: fgr.newFigRenderer(atlasSize, SiwinRenderBackend(), pixelScale) + ) + except Exception: + nil + +proc siwinBackendNameForRendererBinding(renderer: SiwinRendererRef): string = + if renderer.isNil or renderer.inner.isNil: + return "" + try: + siwinRendererBackendNameStorage = siwinBackendName(renderer.inner) + siwinRendererBackendNameStorage + except Exception: + "" + +proc newSiwinWindowBinding( + width, height: int32, + fullscreen: bool, + title: string, + vsync: bool, + msaa: int32, + resizable: bool, + frameless: bool, + transparent: bool, +): SiwinWindowRef = + try: + SiwinWindowRef( + inner: newSiwinWindow( + size = ivec2(width, height), + fullscreen = fullscreen, + title = title, + vsync = vsync, + msaa = msaa, + resizable = resizable, + frameless = frameless, + transparent = transparent, + ) + ) + except Exception: + nil + +proc newSiwinWindowForRendererBinding( + renderer: SiwinRendererRef, + width, height: int32, + fullscreen: bool, + title: string, + vsync: bool, + msaa: int32, + resizable: bool, + frameless: bool, + transparent: bool, +): SiwinWindowRef = + if renderer.isNil or renderer.inner.isNil: + return nil + try: + SiwinWindowRef( + inner: newSiwinWindow( + renderer = renderer.inner, + size = ivec2(width, height), + fullscreen = fullscreen, + title = title, + vsync = vsync, + msaa = msaa, + resizable = resizable, + frameless = frameless, + transparent = transparent, + ) + ) + except Exception: + nil + +proc closeWindowBinding(window: SiwinWindowRef) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.close() + except Exception: + discard + +proc stepWindowBinding(window: SiwinWindowRef) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.step() + except Exception: + discard + +proc firstStepWindowBinding(window: SiwinWindowRef, makeVisible = true) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.firstStep(makeVisible) + except Exception: + discard + +proc redrawWindowBinding(window: SiwinWindowRef) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.redraw() + except Exception: + discard + +proc makeCurrentWindowBinding(window: SiwinWindowRef) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.makeCurrent() + except Exception: + discard + +proc windowIsOpenBinding(window: SiwinWindowRef): bool = + if window.isNil or window.inner.isNil: + return false + window.inner.opened() + +proc siwinDisplayServerNameBinding(window: SiwinWindowRef): string = + if window.isNil or window.inner.isNil: + return "" + try: + siwinDisplayNameStorage = siwinDisplayServerName(window.inner) + siwinDisplayNameStorage + except Exception: + "" + +proc siwinWindowTitleForRendererBinding( + renderer: SiwinRendererRef, window: SiwinWindowRef, suffix: string +): string = + if renderer.isNil or renderer.inner.isNil or window.isNil or window.inner.isNil: + return "" + try: + siwinRendererWindowTitleStorage = + siwinWindowTitle(renderer.inner, window.inner, suffix = suffix) + siwinRendererWindowTitleStorage + except Exception: + "" + +proc backingWidthBinding(window: SiwinWindowRef): int32 = + if window.isNil or window.inner.isNil: + return 0'i32 + try: + window.inner.backingSize().x + except Exception: + 0'i32 + +proc backingHeightBinding(window: SiwinWindowRef): int32 = + if window.isNil or window.inner.isNil: + return 0'i32 + try: + window.inner.backingSize().y + except Exception: + 0'i32 + +proc logicalWidthBinding(window: SiwinWindowRef): float32 = + if window.isNil or window.inner.isNil: + return 0'f32 + try: + window.inner.logicalSize().x + except Exception: + 0'f32 + +proc logicalHeightBinding(window: SiwinWindowRef): float32 = + if window.isNil or window.inner.isNil: + return 0'f32 + try: + window.inner.logicalSize().y + except Exception: + 0'f32 + +proc contentScaleBinding(window: SiwinWindowRef): float32 = + if window.isNil or window.inner.isNil: + return 1'f32 + try: + window.inner.contentScale() + except Exception: + 1'f32 + +proc configureUiScaleBinding(window: SiwinWindowRef, envVar: string): bool = + if window.isNil or window.inner.isNil: + return false + try: + window.inner.configureUiScale(envVar = envVar) + except Exception: + false + +proc refreshUiScaleBinding(window: SiwinWindowRef, autoScale: bool) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.refreshUiScale(autoScale) + except Exception: + discard + +proc presentNowBinding(window: SiwinWindowRef) = + if window.isNil or window.inner.isNil: + return + try: + window.inner.presentNow() + except Exception: + discard + +proc setupBackendBinding(renderer: SiwinRendererRef, window: SiwinWindowRef) = + if renderer.isNil or renderer.inner.isNil or window.isNil or window.inner.isNil: + return + try: + setupBackend(renderer.inner, window.inner) + except Exception: + discard + +proc beginFrameBinding(renderer: SiwinRendererRef) = + if renderer.isNil or renderer.inner.isNil: + return + try: + beginFrame(renderer.inner) + except Exception: + discard + +proc endFrameBinding(renderer: SiwinRendererRef) = + if renderer.isNil or renderer.inner.isNil: + return + try: + endFrame(renderer.inner) + except Exception: + discard + +proc renderFrameBinding( + renderer: SiwinRendererRef, renders: Renders, width, height: float32 +) = + if renderer.isNil or renderer.inner.isNil or renders.isNil: + return + try: + renderer.inner.renderFrame(renders.inner, vec2(width, height)) + except Exception: + discard + +when defined(macosx) and UseMetalBackend: + proc attachMetalLayerBinding( + window: SiwinWindowRef, devicePtr: uint64 + ): SiwinMetalLayerRef = + if window.isNil or window.inner.isNil or devicePtr == 0'u64: + return nil + try: + SiwinMetalLayerRef( + inner: attachMetalLayer( + window.inner, + cast[siwinmetal.MTLDevice](cast[pointer](devicePtr)), + ) + ) + except Exception: + nil + + proc updateMetalLayerBinding(layer: SiwinMetalLayerRef, window: SiwinWindowRef) = + if layer.isNil or window.isNil or window.inner.isNil: + return + try: + updateMetalLayer(layer.inner, window.inner) + except Exception: + discard + + proc setOpaqueBinding(layer: SiwinMetalLayerRef, opaque: bool) = + if layer.isNil: + return + try: + siwinshim.setOpaque(layer.inner, opaque) + except Exception: + discard + +exportRefObject SiwinWindowRef: + procs: + closeWindowBinding(SiwinWindowRef) + firstStepWindowBinding(SiwinWindowRef, bool) + stepWindowBinding(SiwinWindowRef) + redrawWindowBinding(SiwinWindowRef) + makeCurrentWindowBinding(SiwinWindowRef) + windowIsOpenBinding(SiwinWindowRef) + siwinDisplayServerNameBinding(SiwinWindowRef) + backingWidthBinding(SiwinWindowRef) + backingHeightBinding(SiwinWindowRef) + logicalWidthBinding(SiwinWindowRef) + logicalHeightBinding(SiwinWindowRef) + contentScaleBinding(SiwinWindowRef) + configureUiScaleBinding(SiwinWindowRef, string) + refreshUiScaleBinding(SiwinWindowRef, bool) + presentNowBinding(SiwinWindowRef) + +exportRefObject SiwinRendererRef: + procs: + siwinBackendNameForRendererBinding(SiwinRendererRef) + setupBackendBinding(SiwinRendererRef, SiwinWindowRef) + beginFrameBinding(SiwinRendererRef) + endFrameBinding(SiwinRendererRef) + renderFrameBinding(SiwinRendererRef, Renders, float32, float32) + +when defined(macosx) and UseMetalBackend: + exportRefObject SiwinMetalLayerRef: + procs: + updateMetalLayerBinding(SiwinMetalLayerRef, SiwinWindowRef) + setOpaqueBinding(SiwinMetalLayerRef, bool) + +exportProcs: + siwinBackendNameBinding + siwinWindowTitleBinding + sharedSiwinGlobalsPtrBinding + newSiwinRendererBinding + newSiwinWindowBinding + newSiwinWindowForRendererBinding + +when defined(macosx) and UseMetalBackend: + exportProcs: + attachMetalLayerBinding diff --git a/config.nims b/config.nims index e3ee779..1f5abf5 100644 --- a/config.nims +++ b/config.nims @@ -59,3 +59,26 @@ task test_emscripten, "build emscripten examples": for file in listFiles("examples"): if file.startsWith("examples/windy_") and file.endsWith(".nim"): nimExec("c", file, "-d:emscripten") + +task bindings, "Generate bindings": + let includeSiwinShim = + getEnv("FIGDRAW_BINDINGS_SIWINSHIM").strip().toLowerAscii() in + ["1", "true", "yes", "on"] + let siwinShimFlag = if includeSiwinShim: " -d:figdraw.bindings.siwinshim" else: "" + + proc compile(libName: string, flags = "") = + exec "nim c -f " & flags & + siwinShimFlag & + " --path:src -d:release --app:lib --gc:arc --tlsEmulation:off --out:" & libName & + " --outdir:bindings/generated bindings/bindings.nim" + + when defined(windows): + compile "figdraw.dll" + elif defined(macosx): + compile "libfigdraw.dylib.arm", + "--cpu:arm64 -l:'-target arm64-apple-macos11' -t:'-target arm64-apple-macos11'" + compile "libfigdraw.dylib.x64", + "--cpu:amd64 -l:'-target x86_64-apple-macos10.12' -t:'-target x86_64-apple-macos10.12'" + exec "lipo bindings/generated/libfigdraw.dylib.arm bindings/generated/libfigdraw.dylib.x64 -output bindings/generated/libfigdraw.dylib -create" + else: + compile "libfigdraw.so" diff --git a/examples/python/.gitignore b/examples/python/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/examples/python/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/examples/python/siwin_renderlist_100.py b/examples/python/siwin_renderlist_100.py new file mode 100644 index 0000000..e2697c4 --- /dev/null +++ b/examples/python/siwin_renderlist_100.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +import math +import os +import sys +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +BINDINGS_DIR = ROOT / "bindings" / "generated" +sys.path.insert(0, str(BINDINGS_DIR)) + +# genny currently emits fig_draw.py expecting libfig_draw.*, while config.nims +# outputs libfigdraw.*. Create a local symlink/copy fallback before import. +if sys.platform == "darwin": + expected = BINDINGS_DIR / "libfig_draw.dylib" + actual = BINDINGS_DIR / "libfigdraw.dylib" + if (not expected.exists()) and actual.exists(): + try: + expected.symlink_to(actual.name) + except OSError: + expected.write_bytes(actual.read_bytes()) +elif sys.platform.startswith("linux"): + expected = BINDINGS_DIR / "libfig_draw.so" + actual = BINDINGS_DIR / "libfigdraw.so" + if (not expected.exists()) and actual.exists(): + try: + expected.symlink_to(actual.name) + except OSError: + expected.write_bytes(actual.read_bytes()) +elif sys.platform == "win32": + expected = BINDINGS_DIR / "fig_draw.dll" + actual = BINDINGS_DIR / "figdraw.dll" + if (not expected.exists()) and actual.exists(): + expected.write_bytes(actual.read_bytes()) + +import fig_draw as fd + + +COPIES = 100 +RUN_ONCE = os.getenv("FIGDRAW_RUN_ONCE", "").strip().lower() in {"1", "true", "yes"} +NO_SLEEP = os.getenv("FIGDRAW_NO_SLEEP", "1").strip().lower() in {"1", "true", "yes"} + +RED_FILL = fd.RgbaColor(220, 40, 40, 155) +RED_STROKE = fd.RgbaColor(0, 0, 0, 155) +GREEN_SOLID = fd.RgbaColor(40, 180, 90, 155) +GREEN_GRAD_START = fd.RgbaColor(18, 112, 64, 255) +GREEN_GRAD_MID = fd.RgbaColor(40, 180, 90, 255) +GREEN_GRAD_STOP = fd.RgbaColor(78, 224, 188, 255) +BLUE_SOLID = fd.RgbaColor(60, 90, 220, 155) +BLUE_GRAD_START = fd.RgbaColor(44, 72, 186, 255) +BLUE_GRAD_MID = fd.RgbaColor(60, 90, 220, 255) +BLUE_GRAD_STOP = fd.RgbaColor(118, 168, 255, 255) +WHITE_STROKE = fd.RgbaColor(255, 255, 255, 210) +BLACK_SHADOW = fd.RgbaColor(0, 0, 0, 155) +BLUE_INNER_SHADOW = fd.RgbaColor(40, 40, 60, 150) + + +def make_render_tree(width: float, height: float, frame: int) -> fd.Renders: + renders = fd.Renders() + t = frame * 0.02 + + background = fd.new_rectangle_fig(0.0, 0.0, width, height) + background.set_fill_color(255, 255, 255, 155) + renders.add_root(0, background) + + red_start_x = 60.0 + red_start_y = 60.0 + green_start_x = 320.0 + green_start_y = 120.0 + blue_start_x = 180.0 + blue_start_y = 300.0 + max_w = 260.0 + max_h = 180.0 + max_x = max(0.0, width - (green_start_x + max_w)) + max_y = max(0.0, height - (blue_start_y + max_h)) + + for i in range(COPIES): + seed_x = math.sin(i * 78.233) * 43758.5453 + seed_y = math.sin((i + 19) * 37.719) * 24634.6345 + base_x = (seed_x - math.floor(seed_x)) * max_x if max_x > 0 else 0.0 + base_y = (seed_y - math.floor(seed_y)) * max_y if max_y > 0 else 0.0 + + jitter_x = math.sin(t + i * 0.15) * 20.0 + jitter_y = math.cos(t * 0.9 + i * 0.2) * 20.0 + offset_x = min(max(base_x + jitter_x, 0.0), max_x) + offset_y = min(max(base_y + jitter_y, 0.0), max_y) + + size_pulse_w = 0.5 + 0.5 * math.sin(t * 0.8 + i * 0.07) + size_pulse_h = 0.5 + 0.5 * math.cos(t * 0.65 + i * 0.09) + + red_w = 160.0 + 100.0 * size_pulse_w + red_h = 110.0 + 70.0 * size_pulse_h + green_w = 160.0 + 100.0 * size_pulse_h + green_h = 110.0 + 70.0 * size_pulse_w + blue_w = 160.0 + 100.0 * (1.0 - size_pulse_w) + blue_h = 110.0 + 70.0 * (1.0 - size_pulse_h) + + red_fig = fd.new_rectangle_fig(red_start_x + offset_x, red_start_y + offset_y, red_w, red_h) + red_fig.set_fill_color_rgba(RED_FILL) + corner_pulse = 0.5 + 0.5 * math.sin(t * 1.25 + i * 0.11) + c0 = 4.0 + 26.0 * corner_pulse + c1 = 6.0 + 22.0 * (1.0 - corner_pulse) + c2 = 8.0 + 18.0 * (0.5 + 0.5 * math.sin(t * 0.7 + i * 0.05)) + c3 = 10.0 + 16.0 * (0.5 + 0.5 * math.cos(t * 0.8 + i * 0.06)) + red_fig.set_corners(c0, c1, c2, c3) + red_fig.set_stroke_rgba(5.0, RED_STROKE) + renders.add_root(0, red_fig) + + green_fig = fd.new_rectangle_fig( + green_start_x + offset_x, green_start_y + offset_y, green_w, green_h + ) + use_green_gradient = (i % 2) == 0 + if use_green_gradient: + green_axis = 0 if (i % 4) < 2 else 2 # X or DiagTLBR + green_fig.set_fill_linear3_rgba( + GREEN_GRAD_START, GREEN_GRAD_MID, GREEN_GRAD_STOP, green_axis, 128 + ) + else: + green_fig.set_fill_color_rgba(GREEN_SOLID) + green_corner_pulse = 0.5 + 0.5 * math.cos(t * 0.95 + i * 0.08) + g0 = 6.0 + 22.0 * green_corner_pulse + g1 = 8.0 + 18.0 * (1.0 - green_corner_pulse) + g2 = 10.0 + 16.0 * (0.5 + 0.5 * math.cos(t * 0.75 + i * 0.04)) + g3 = 12.0 + 14.0 * (0.5 + 0.5 * math.sin(t * 0.85 + i * 0.05)) + green_fig.set_corners(g0, g1, g2, g3) + shadow_pulse = 0.5 + 0.5 * math.sin(t * 1.1 + i * 0.05) + shadow_blur = max(0.0, 6.0 + 18.0 * shadow_pulse) + shadow_spread = max(0.0, 4.0 + 20.0 * (1.0 - shadow_pulse)) + shadow_x = 6.0 + 10.0 * math.sin(t * 0.9 + i * 0.03) + shadow_y = 6.0 + 10.0 * math.cos(t * 0.9 + i * 0.03) + green_fig.clear_shadows() + green_fig.set_shadow_rgba( + 0, # first shadow slot + 1, # DropShadow + shadow_blur, + shadow_spread, + shadow_x, + shadow_y, + BLACK_SHADOW, + ) + renders.add_root(0, green_fig) + + blue_fig = fd.new_rectangle_fig(blue_start_x + offset_x, blue_start_y + offset_y, blue_w, blue_h) + use_blue_gradient = (i % 3) == 0 + if use_blue_gradient: + blue_axis = 1 if (i % 2) == 0 else 3 # Y or DiagBLTR + blue_fig.set_fill_linear3_rgba( + BLUE_GRAD_START, BLUE_GRAD_MID, BLUE_GRAD_STOP, blue_axis, 132 + ) + else: + blue_fig.set_fill_color_rgba(BLUE_SOLID) + blue_fig.set_stroke_rgba(4.0, WHITE_STROKE) + inset_pulse = 0.5 + 0.5 * math.sin(t * 1.05 + i * 0.06) + inset_blur = max(0.0, 8.0 + 10.0 * inset_pulse) + inset_spread = max(0.0, 2.0 + 10.0 * (1.0 - inset_pulse)) + inset_x = 6.0 * math.sin(t * 0.85 + i * 0.04) + inset_y = 6.0 * math.cos(t * 0.8 + i * 0.04) + blue_fig.clear_shadows() + blue_fig.set_shadow_rgba( + 0, # first shadow slot + 2, # InnerShadow + inset_blur, + inset_spread, + inset_x, + inset_y, + BLUE_INNER_SHADOW, + ) + renders.add_root(0, blue_fig) + + return renders + + +def main() -> int: + fd.set_fig_data_dir(str(ROOT / "data")) + + typeface = fd.load_typeface_binding("Ubuntu.ttf") + if not typeface: + print("Failed to load typeface: Ubuntu.ttf") + return 1 + fps_font = fd.FigFontRef(typeface, 18.0) + fps_text = "0.0 FPS" + + renderer = fd.new_siwin_renderer_binding(512, 1.0) + if not renderer: + print("Failed to create Siwin renderer") + return 1 + + title = "Siwin RenderList (Python)" + window = fd.new_siwin_window_binding(800, 600, False, title, True, 0, True, False, False) + if not window: + print("Failed to create Siwin window") + return 1 + + auto_scale = window.configure_ui_scale_binding("") + renderer.setup_backend_binding(window) + window.first_step_window_binding(True) + window.make_current_window_binding() + print("Backend initialized") + + frames = 0 + fps_frames = 0 + frame = 0 + fps_start = time.time() + make_render_tree_ms_sum = 0.0 + render_frame_ms_sum = 0.0 + last_element_count = 0 + + try: + while window.window_is_open_binding(): + window.refresh_ui_scale_binding(auto_scale) + + frame += 1 + frames += 1 + fps_frames += 1 + + renderer.begin_frame_binding() + width = window.logical_width_binding() + height = window.logical_height_binding() + + t0 = time.perf_counter() + renders = make_render_tree(width, height, frame) + make_render_tree_ms_sum += (time.perf_counter() - t0) * 1000.0 + last_element_count = renders.layer_node_count(0) + + hud_margin = 12.0 + hud_w = 180.0 + hud_h = 34.0 + hud_x = width - hud_w - hud_margin + hud_y = hud_margin + hud_rect = fd.new_rectangle_fig(hud_x, hud_y, hud_w, hud_h) + hud_rect.set_fill_color(0, 0, 0, 155) + hud_rect.set_corners(8.0, 8.0, 8.0, 8.0) + renders.add_root(0, hud_rect) + + hud_text_pad_x = 10.0 + hud_text_pad_y = 6.0 + hud_text_x = hud_x + hud_text_pad_x + hud_text_y = hud_y + hud_text_pad_y + hud_text_w = hud_w - hud_text_pad_x * 2.0 + hud_text_h = hud_h - hud_text_pad_y * 2.0 + + fps_layout = fd.typeset_text_binding( + hud_text_w, + hud_text_h, + fps_font, + fps_text, + h_align=2, # Right + v_align=1, # Middle + min_content=False, + wrap=False, + ) + if fps_layout: + hud_text = fd.new_text_fig(hud_text_x, hud_text_y, hud_text_w, hud_text_h) + hud_text.set_fill_color(0, 0, 0, 0) + fd.set_fig_text_layout_binding(hud_text, fps_layout) + renders.add_root(0, hud_text) + + t1 = time.perf_counter() + renderer.render_frame_binding(renders, width, height) + render_frame_ms_sum += (time.perf_counter() - t1) * 1000.0 + renderer.end_frame_binding() + window.present_now_binding() + + window.redraw_window_binding() + window.step_window_binding() + + now = time.time() + elapsed = now - fps_start + if elapsed >= 1.0: + fps = fps_frames / elapsed + fps_text = f"{fps:0.1f} FPS" + avg_make = make_render_tree_ms_sum / max(1, fps_frames) + avg_render = render_frame_ms_sum / max(1, fps_frames) + print( + "fps:", + f"{fps:.1f}", + "| elems:", + last_element_count, + "| makeRenderTree avg(ms):", + f"{avg_make:.3f}", + "| renderFrame avg(ms):", + f"{avg_render:.3f}", + ) + fps_frames = 0 + fps_start = now + make_render_tree_ms_sum = 0.0 + render_frame_ms_sum = 0.0 + + if RUN_ONCE and frames >= 1: + break + if (not NO_SLEEP) and window.window_is_open_binding(): + time.sleep(0.016) + finally: + window.close_window_binding() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/figdraw.nimble b/figdraw.nimble index db8f2b1..bc6db7c 100644 --- a/figdraw.nimble +++ b/figdraw.nimble @@ -43,4 +43,6 @@ feature "vulkan": feature "metal": requires "https://github.com/elcritch/metalx#head" +feature "sharedlib": + requires "genny"