# SPDX-FileCopyrightText: ☭ 2021 Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[asyncdispatch, hashes, options, os, tables, xmltree] import preserves, preserves/parse, preserves/xmlhooks import syndicate, syndicate/[actors, capabilities, dataspaces, patterns, relay], syndicate/protocols/[simpleChatProtocol] import nimsvg import bumpy, pixie, pixie/fileformats/svg import sdl2 import svui, svui/render/xhtml type Svui = svui.Svui[Ref] SdlError = object of CatchableError Attrs = Table[string, Assertion] func toVec2(p: Point): Vec2 = vec2(float p.x, float p.y) template check(res: cint) = if res != 0: let msg = $sdl2.getError() raise newException(SdlError, msg) template check(res: SDL_Return) = if res == SdlError: let msg = $sdl2.getError() raise newException(SdlError, msg) const amask = uint32 0xff000000 rmask = uint32 0x000000ff gmask = uint32 0x0000ff00 bmask = uint32 0x00ff0000 type PaneKind = enum pkSvg, pkXhtml Pane = ref object texture: TexturePtr rect: bumpy.Rect case kind: PaneKind of pkSvg: svg: string of pkXhtml: xhtml: Xhtml App = ref object screen: Image window: WindowPtr renderer: RendererPtr panes: Table[Hash, Pane] viewPoint: Vec2 zoomFactor: float typesetting: Typesetting proc newPane(app: App; svg: string): Pane = let (w, h) = app.window.getSize result = Pane(kind: pkSvg, svg: svg, rect: rect(0, 0, float w, float h)) var image = decodeSvg(svg, w, h) dataPtr = image.data[0].addr surface = createRGBSurfaceFrom( dataPtr, cint w, h, cint 32, cint 4*w, rmask, gmask, bmask, amask) result.texture = createTextureFromSurface(app.renderer, surface) destroy surface #[ result.texture = createTexture( app.renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, w div 2, h div 2) updateTexture(result.texture, nil, image.data[0].addr, cint 32) ]# func rect(img: Image): bumpy.Rect = result.w = float img.width result.h = float img.height proc newPane(app: App; xhtml: Xhtml): Pane = var image = render(app.typesetting, xhtml) dataPtr = image.data[0].addr surface = createRGBSurfaceFrom( dataPtr, cint image.width, cint image.height, cint 32, cint 4*image.width, rmask, gmask, bmask, amask) result = Pane( kind: pkXhtml, xhtml: xhtml, rect: image.rect, texture: createTextureFromSurface(app.renderer, surface)) destroy surface assert(result.rect.w != 0) assert(result.rect.h != 0) proc newApp(length: cint): App = ## Create a new square plane of `length` pixels. result = App(zoomFactor: 1.0, typesetting: initTypesetting()) discard createWindowAndRenderer( length, length, SDL_WINDOW_RESIZABLE, result.window, result.renderer) var info: RendererInfo check getRendererInfo(result.renderer, addr info) echo "SDL Renderer: ", info.name func toSdl(rect: bumpy.Rect): sdl2.Rect = (result.x, result.y, result.w, result.h) = (cint rect.x, cint rect.y, cint rect.w, cint rect.h) proc viewPort(app: App; wh: Vec2): bumpy.Rect = result.wh = wh / app.zoomFactor result.xy = app.viewPoint - (result.wh * 0.5) proc redraw(app: App) = assert app.zoomFactor != 0.0 var (w, h) = app.window.getSize sdlViewPort = rect(-float(w shr 1), -float(h shr 1), float w, float h) viewPort = app.viewPort(sdlViewPort.wh) app.renderer.setDrawColor(0x40, 0x40, 0x40) app.renderer.clear() for pane in app.panes.values: if overlaps(viewPort, pane.rect): var overlap = viewPort and pane.rect src = rect(overlap.xy - pane.rect.xy, overlap.wh) dst: bumpy.Rect dst.x = (overlap.x - viewPort.x) * (sdlViewPort.w / viewPort.w) dst.y = (overlap.y - viewPort.y) * (sdlViewPort.h / viewPort.h) dst.wh = if app.zoomFactor == 1.0: overlap.wh # correct elif app.zoomFactor > 1.0: sdlViewPort.wh - dst.xy # correct? else: overlap.wh * app.zoomFactor var (sdlSrc, sdlDst) = (src.toSdl, dst.toSdl) app.renderer.copy(pane.texture, addr sdlSrc, addr sdlDst) app.renderer.present() proc resize(app: App) = ## Resize to new dimensions of the SDL window. redraw(app) proc zoom(app: App; change: float) = app.zoomFactor = app.zoomFactor * (1.0 + ((1 / 8) * change)) app.redraw() proc pan(app: App; xy: Vec2) = app.viewPoint.xy = app.viewPoint.xy + (xy / app.zoomFactor) app.redraw() proc recenter(app: App) = reset app.viewPoint app.zoomFactor = 1.0 app.redraw() proc main() = discard sdl2.init(INIT_TIMER or INIT_VIDEO or INIT_EVENTS) let app = newApp(512) app.redraw() let cap = mint() asyncCheck runActor("chat") do (turn: var Turn): connectUnix(turn, "/run/syndicate/ds", cap) do (turn: var Turn; a: Assertion) -> TurnAction: let ds = unembed a onPublish(turn, ds, Svui ? {0: drop(), 1: grab()}) do (content: Content): case content.orKind of ContentKind.Xhtml: # content.xhtml onRetract: app.panes.del(hash content.xhtml) app.redraw() # TODO: keep dead panes around until they are cleaned up. # If something crashes then the last state should be visable. var pane = app.panes.getOrDefault(hash content.xhtml) if pane.isNil: #try: block: pane = newPane(app, content.xhtml) app.panes[hash content.xhtml] = pane #except: # let msg = getCurrentExceptionMsg() # stderr.writeLine "failed to render XHMTL: ", msg else: pane.xhtml = content.xhtml app.redraw() # TODO: wait til end of turn of ContentKind.Svg: let xn = preserveTo(toPreserve(content.xhtml), xmltree.XmlNode) let svg = $(get xn) onRetract: app.panes.del(hash svg) app.redraw() # TODO: keep dead panes around until they are cleaned up. # If something crashes then the last state should be visable. var pane = app.panes.getOrDefault(hash svg) if pane.isNil: try: pane = newPane(app, svg) app.panes[hash svg] = pane except: let msg = getCurrentExceptionMsg() stderr.writeLine "failed to render SVG: ", msg else: pane.svg = svg app.redraw() # TODO: wait til end of turn const sdlTimeout = 500 asyncPollTimeout = 500 var evt = sdl2.defaultEvent mousePanning: bool while true: asyncdispatch.poll(0) if waitEventTimeout(evt, sdlTimeout): case evt.kind of MouseWheel: app.zoom(evt.wheel.y.float) of WindowEvent: if evt.window.event == WindowEvent_Resized: app.resize() of MouseMotion: if mousePanning: var xy = vec2(evt.motion.xrel.float, evt.motion.yrel.float) app.pan(xy * 2.0) of MouseButtonDown: case evt.button.button of BUTTON_MIDDLE: mousePanning = true else: discard of MouseButtonUp: case evt.button.button of BUTTON_MIDDLE: mousePanning = false else: discard of KeyUp: try: let code = evt.key.keysym.scancode.Scancode echo "code: ", code if code in {SDL_SCANCODE_SPACE, SDL_SCANCODE_ESCAPE}: app.recenter() except: # invalid event.key.keysym.sym sometimes arrive discard of KeyDown: discard of QuitEvent: quit(0) else: echo evt.kind main()