commit f344b29e1e940fa652500f762b1ac96a2a373e71 Author: Emery Hemingway Date: Fri Sep 8 10:29:04 2023 +0200 On the train to Praha diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..d324c24 --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +source_env .. +use nix diff --git a/.nimble b/.nimble new file mode 100644 index 0000000..71bbca8 --- /dev/null +++ b/.nimble @@ -0,0 +1,10 @@ +let + syndicate = builtins.getFlake "syndicate"; + pkgs = + import { overlays = (builtins.attrValues syndicate.overlays); }; +in pkgs.nim2Packages.buildNimPackage (finalAttrs: prevAttrs: { + pname = "immutulator"; + version = "unstable"; + nativeBuildInputs = [ pkgs.pkg-config ]; + propagatedBuildInputs = [ pkgs.nim2Packages.getdns pkgs.nim2Packages.sdl2 ]; +}) diff --git a/Tuprules.tup b/Tuprules.tup new file mode 100644 index 0000000..05ca8fd --- /dev/null +++ b/Tuprules.tup @@ -0,0 +1,19 @@ +include ../syndicate-nim/depends.tup + +NIM_FLAGS += --path:$(TUP_CWD)/../bumpy/src +NIM_FLAGS += --path:$(TUP_CWD)/../chroma/src +NIM_FLAGS += --path:$(TUP_CWD)/../crunchy/src +NIM_FLAGS += --path:$(TUP_CWD)/../nimsimd/src +NIM_FLAGS += --path:$(TUP_CWD)/../nimsvg/src +NIM_FLAGS += --path:$(TUP_CWD)/../pixie/src +NIM_FLAGS += --path:$(TUP_CWD)/../svui/nim +NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src +NIM_FLAGS += --path:$(TUP_CWD)/../vmath/src +NIM_FLAGS += --path:$(TUP_CWD)/../zippy/src + +NIM_FLAGS += --path:%/src +NIM_GROUPS += $(TUP_CWD)/../nimble/ + +# NIM_FLAGS += --path:%/src +# NIM_GROUPS += $(TUP_CWD)/../nimble/ +NIM_FLAGS += --path:$(TUP_CWD)/../sdl2-nim/src diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..a91bd28 --- /dev/null +++ b/shell.nix @@ -0,0 +1,10 @@ +let + syndicate = builtins.getFlake "syndicate"; + pkgs = + import { overlays = (builtins.attrValues syndicate.overlays); }; +in pkgs.nim2Packages.buildNimPackage (finalAttrs: prevAttrs: { + pname = "well_of_text"; + version = "unstable"; + nativeBuildInputs = [ pkgs.pkg-config ]; + propagatedBuildInputs = [ pkgs.nim2Packages.getdns pkgs.nim2Packages.sdl2 ]; +}) diff --git a/src/Tupfile b/src/Tupfile new file mode 100644 index 0000000..4d269aa --- /dev/null +++ b/src/Tupfile @@ -0,0 +1,4 @@ +include_rules +: well_of_text.nim |> !nim_bin |> {bin} +: text_spans.nim |> !nim_bin |> {bin} +: wells.nim |> !nim_bin |> {bin} diff --git a/src/frames.nim b/src/frames.nim new file mode 100644 index 0000000..1d162b2 --- /dev/null +++ b/src/frames.nim @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[deques] +import pixie + +type + Pane* = ref object + image: pixie.Image + spans: seq[Span] + arrangement: Arrangement + + Well* = ref object + panes: Deque[Pane] + +proc newFrame(width, height: int): Frame = + let (w, h) = (width div 2, height div 2) + new result + result.panes[0] = Pane(image: newImage(w, h)) + result.panes[1] = Pane(image: newImage(w, h)) + result.panes[2] = Pane(image: newImage(w, h)) + +proc paneVec2(frame: Frame): Vec2 = + vec2(frame.panes[0].image.width.float32, frame.panes[0].image.height.float32) + +proc allocChild(frame: Frame) = + assert frame.child.isNil + let (w, h) = (frame.panes[0].image.width div 2, frame.panes[0].image.height div 2) + doAssert w > 2 and h > 2, $w & "x" & $h + frame.child = newFrame(w, h) + +proc setDepth(frame: Frame; n: Natural) = + var + frame = frame + i = n + while i > 0: + dec(i) + if frame.child.isNil: + allocChild(frame) + frame = frame.child + +proc `[]`*(frame: Frame; i: Natural): Pane = + var + frame = frame + i = i + while true: + if i < 4: + return frame.panes[i] + if frame.child.isNil: + allocChild(frame) + frame = frame.child + dec(i, 4) + +iterator walkPanes(frame: Frame): tuple[coord: Vec2, pane: Pane] = + var frame = frame + while not frame.isNil and + frame.panes[0].image.width > 2 and + frame.panes[1].image.height > 2: + let coord = frame.paneVec2 + yield(coord, frame.panes[0]) + yield(coord - vec2(coord.x, 0), frame.panes[1]) + yield(coord - vec2(0, coord.y), frame.panes[2]) + frame = frame.child + +proc append*(frame: Fram; text: string; font: Font) = + let span = newSpan(text, font) + +proc append*(frame: Fram; stream: Stream; font: Font) = + var line: string + while readLine(stream, line): + append(frame, font, line) + +when isMainModule: + let frame = newFrame(800, 600) + frame.setDepth(3) + for (coord, pane) in walkPanes(frame): + echo coord, " - ", pane.image.width, "x", pane.image.height diff --git a/src/text_spans.nim b/src/text_spans.nim new file mode 100644 index 0000000..81c946f --- /dev/null +++ b/src/text_spans.nim @@ -0,0 +1,26 @@ +import pixie + +let image = newImage(200, 200) +image.fill(rgba(255, 255, 255, 255)) + +const typefacePath = + "/nix/store/ay5vhxszmibk0nrhx1vid4nhgvgdniq8-corefonts-1/share/fonts/truetype/Trebuchet_MS.ttf" + +let typeface = readTypeface(typefacePath) + +proc newFont(typeface: Typeface, size: float32, color: Color): Font = + result = newFont(typeface) + result.size = size + result.paint.color = color + +let spans = @[ + newSpan("verb [with object] ", + newFont(typeface, 12, color(0.78125, 0.78125, 0.78125, 1))), + newSpan("strallow\n", newFont(typeface, 36, color(0, 0, 0, 1))), + newSpan("\nstral·low\n", newFont(typeface, 13, color(0, 0.5, 0.953125, 1))), + newSpan("\n1. free (something) from restrictive restrictions \"the regulations are intended to strallow changes in public policy\" ", + newFont(typeface, 14, color(0.3125, 0.3125, 0.3125, 1))) +] + +image.fillText(typeset(spans, vec2(180, 180)), translate(vec2(10, 10))) +image.writeFile("text_spans.png") diff --git a/src/typesetting.nim b/src/typesetting.nim new file mode 100644 index 0000000..b8f4c7a --- /dev/null +++ b/src/typesetting.nim @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[sequtils] +import pixie + +proc newFont*(typeface: Typeface, size: float32): Font = + result = newFont(typeface) + result.size = size + +func height(arr: Arrangement): float = + if arr.positions.len > 0: result = arr.positions[arr.positions.high].y + +proc add(result: var Arrangement; other: sink Arrangement) = + if result.lines.len == 0: + result = other + else: + let runeOff = result.runes.len + add(result.lines, map(other.lines, + proc (x: (int, int)): (int, int) = (x[0]+runeOff, x[1]+runeOff))) + add(result.spans, map(other.spans, + proc (x: (int, int)): (int, int) = (x[0]+runeOff, x[1]+runeOff))) + add(result.fonts, other.fonts) + add(result.runes, other.runes) + let yOff = result.positions[result.positions.high].y + add(result.positions, + map(other.positions, + proc(pos: Vec2): Vec2 = vec2(pos.x, pos.y + yOff))) + add(result.selectionRects, + map(other.selectionRects, + proc(rect: Rect): Rect = rect(rect.x, rect.y + yOff, rect.w, rect.h))) + +proc render*(font: Font; text: string): Image = + # TODO: render by font size, not by wh + var wh = layoutBounds(font, "X") + echo "bounds of X are ", wh + wh.x = wh.x * 80 + wh.y = wh.y * 50 + let margin = wh / 9.0 + var + printSpace = wh * (7.0 / 9.0) + pages = @[Arrangement()] + + proc pageEnd(): float = + let + pi = pages.high + li = pages[pi].lines.high + ci = pages[pi].lines[li][1] + pages[pi].positions[ci].y + + proc append(span: Span) = + var arr = typeset(@[span], printSpace) + if pages[pages.high].height + arr.height < printSpace.y: + pages[pages.high].add arr + else: + discard + + append(newSpan(text, font)) + + result = newImage(int wh.x, int wh.y) + fill(result, rgba(255, 255, 255, 255)) + fillText(result, pages[0], translate(margin)) diff --git a/src/well_of_text.nim b/src/well_of_text.nim new file mode 100644 index 0000000..a0daed2 --- /dev/null +++ b/src/well_of_text.nim @@ -0,0 +1,213 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[asyncdispatch, options, os, tables] +import preserves, syndicate, syndicate/[capabilities] +import bumpy, pixie +import sdl2 +import ./wells + +const typefacePath = + "/nix/store/ay5vhxszmibk0nrhx1vid4nhgvgdniq8-corefonts-1/share/fonts/truetype/Trebuchet_MS.ttf" + +proc unixSocketPath: Unix = + result.path = getEnv("SYNDICATE_SOCK") + if result.path == "": + result.path = getEnv("XDG_RUNTIME_DIR", "/run/user/1000") / "dataspace" + +proc envStep: Assertion = + var s = getEnv("SYNDICATE_STEP") + if s != "": parsePreserves(s, Cap) + else: capabilities.mint().toPreserve(Cap) + +type + SdlError = object of CatchableError + +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) + +func toVec2(p: Point): Vec2 = vec2(float p.x, float p.y) + +const + amask = uint32 0xff000000 + rmask = uint32 0x000000ff + gmask = uint32 0x0000ff00 + bmask = uint32 0x00ff0000 + +type + App = ref object + screen: Image + window: WindowPtr + renderer: RendererPtr + texture: TexturePtr + rect: bumpy.Rect + viewPoint: Vec2 + well: Well + zoomFactor: float + +func rect(img: Image): bumpy.Rect = + result.w = float img.width + result.h = float img.height + +proc newApp(length: cint): App = + ## Create a new square plane of `length` pixels. + result = App( + topFrame: newFrame(length shl 1, length shl 1), + zoomFactor: 1.0, + ) + app.well = newWell(length, length) + 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(0x80, 0x80, 0x80) + app.renderer.clear() + + let wellRect = well.Rect + if viewPort.overlaps wellRect: + for (pane, rect) in well.intersectingPanes(viewPort) + let + texture = pane.texture(rect.wh *­app.zoomFactor) + overlap = viewPort and rect + src = rect(overlap.xy - app.rect.xy, overlap.wh) + var 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(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 setImage(app: App; image: Image) = + app.rect = image.rect + echo "create surface of ", image.width, "x", cint image.height, " pixels" + var + dataPtr = image.data[0].addr + surface = createRGBSurfaceFrom( + dataPtr, + cint image.width, cint image.height, + cint 32, cint 4*image.width, + rmask, gmask, bmask, amask) + app.texture = createTextureFromSurface(app.renderer, surface) + +proc main() = + + discard sdl2.init(INIT_TIMER or INIT_VIDEO or INIT_EVENTS) + let app = newApp(512) + + let + typeface = readTypeface(typefacePath) + fontTODO = newFont(typeface, 48) + + app.redraw() + + #[ + let + unix = unixSocketPath() + step = envStep() + let actor = bootDataspace("chat") do (root: Cap; turn: var Turn): + connect(turn, unix, step) do (turn: var Turn; ds: Cap): + echo "connected to syndicate over UNIX-socket" + + asyncCheck actor.future + ]# + + const + sdlTimeout = 500 + asyncPollTimeout = 500 + + let streamTODO = newFileStream(stdin) + + var + evt = sdl2.defaultEvent + mousePanning: bool + lineTODO: string + while true: + if readLine(streamTODO, lineTODO): + app.well.append(line, fontTODO) + app.redraw() + # 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() diff --git a/src/well_of_text.nim.cfg b/src/well_of_text.nim.cfg new file mode 100644 index 0000000..37c9d31 --- /dev/null +++ b/src/well_of_text.nim.cfg @@ -0,0 +1 @@ +debugger:native diff --git a/src/wells.nim b/src/wells.nim new file mode 100644 index 0000000..786fe2a --- /dev/null +++ b/src/wells.nim @@ -0,0 +1,93 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[deques] +import pixie + +type + Pane* = ref object + image: pixie.Image + spans: seq[Span] + arrangement: Arrangement + + Well* = ref object + panes: Deque[Pane] + dimensions: Vec2 + +using + pane: Pane + well: Well + +proc newWell*(width, height: int): Well = + result = Well(dimensions: vec2(float width, float height)) + result.panes.addFirst Pane() + +proc margins*(well): Vec2 = well.dimensions / 9.0 + +proc bounds*(well): Vec2 = well.margin * 7.0 + +proc rect*(well): Rect = + result.wh = well.dimensions + +proc rectAt(well, offset:­int):­Rect = + if offset < well.panes.len: + let n = succ offset + result.w = well.dimensions.x / float(2 * n) + result.h = well.dimensions.y / float(2 * n) + case offset and 3 + of 0: (result.x, result.y) = (result.w, result.h) + of 1: result.y = result.h + of 2: result.x = result.w + else: discard + +proc place*(well; offset, width, height: int): (Pane, Rect) = + ## Return the `Pane` at `offset` from the top of `well` or `nil` if + ## the offset is too deep. The position and size of the `Pane` is + ## returned as well. + if offset < well.panes.len: + result[0] = well.panes[offset] + result[1] = well.rectAt(offset) + +proc append*(well; text: string; font: Font) = + assert well.panes.len > 0 + let span = newSpan(text, font) + while true: + pane = well.panes[0] + pane.spans.add(span) + var + arrangement = typeset(spans, frame.bounds) + bounds = layoutBounds arrangement + assert bounds.x <= well.boundx.x + if bounds.bounds.y <= well.bounds.y: + doAssert pane.spans.len > 1, "text does not find on a single pane" + pane.arrangement = arrangement + break + else: + well.panes.addFirst Pane() + +proc append*(well; stream: Stream; font: Font) = + var line: string + while readLine(stream, line): + append(well, line, font) + +proc render*(well; index: Natural; scale: float): Image = + if index < well.panes.len: + let dimensions = well.dimensions * scale + if dimensions.x > 8 and dimensions.y > 8: + result = newImage(int dimensions.x, int dimensions.y) + fill(result, rgba(255, 255, 255, 255)) + fillText(well.panes[index].arrangment, translate(well.margin)) + + +iterator intersectingPanes(well; rect:­Rect): (Pane, Rect) = + var i = 0 + while i <­well.panes.len: + let bounds = well.boundsAt(i) + if i and 3: + let quad = Rect(0, 0, bounds.x *­2.0, bounds.y * 2.0) + if not rect.overlaps quad: + # all further panes are non-intersecting + break + if rect.overlaps bounds: + yield (well.panes[i], bounds) + inc(i) diff --git a/well_of_text.nimble b/well_of_text.nimble new file mode 100644 index 0000000..ba4d03d --- /dev/null +++ b/well_of_text.nimble @@ -0,0 +1,5 @@ +bin = @["well_of_text"] +license = "Unlicense" +requires: "nim", "syndicate", "sdl2" +srcDir = "src" +version = "20230906"