zua/src/zua.nim

264 lines
7.6 KiB
Nim

# 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()