~xigoi/vier

820563b4fa940f4dfb3695b0a96b1a6242387253 — Adam Blažek 8 months ago c4b4182 master
nph 0.4 formatting
3 files changed, 459 insertions(+), 493 deletions(-)

M src/palettes.nim
M src/utils.nim
M src/vier.nim
M src/palettes.nim => src/palettes.nim +314 -316
@@ 11,319 11,317 @@ proc palette[height: static int](columns: varargs[array[height, Color]]): Image 
      result.imageDrawPixel(x.int32, y.int32, color)

let
  rgbBasic* =
    palette(
      [
        color"#FF0000",
        color"#FF5500",
        color"#FFAA00",
        color"#FFFF00",
        color"#AAFF00",
        color"#55FF00"
      ],
      [
        color"#FF0055",
        color"#00000000",
        color"#00000000",
        color"#00000000",
        color"#00000000",
        color"#00FF00"
      ],
      [
        color"#FF00AA",
        color"#000000",
        color"#555555",
        color"#AAAAAA",
        color"#FFFFFF",
        color"#00FF55"
      ],
      [
        color"#FF00FF",
        color"#000000AA",
        color"#00000055",
        color"#FFFFFF55",
        color"#FFFFFFAA",
        color"#00FFAA"
      ],
      [
        color"#AA00FF",
        color"#5500FF",
        color"#0000FF",
        color"#0055FF",
        color"#00AAFF",
        color"#00FFFF"
      ],
    )
  colar* =
    palette(
      [
        color"#F8FAFB",
        color"#F2F4F6",
        color"#EBEDEF",
        color"#E0E4E5",
        color"#D1D6D8",
        color"#B1B6B9",
        color"#979B9D",
        color"#7E8282",
        color"#666968",
        color"#50514F",
        color"#3A3A37",
        color"#252521",
        color"#121210"
      ],
      [
        color"#FFF5F5",
        color"#FFE3E3",
        color"#FFC9C9",
        color"#FFA8A8",
        color"#FF8787",
        color"#FF6B6B",
        color"#FA5252",
        color"#F03E3E",
        color"#E03131",
        color"#C92A2A",
        color"#B02525",
        color"#962020",
        color"#7D1A1A"
      ],
      [
        color"#FFF0F6",
        color"#FFDEEB",
        color"#FCC2D7",
        color"#FAA2C1",
        color"#F783AC",
        color"#F06595",
        color"#E64980",
        color"#D6336C",
        color"#C2255C",
        color"#A61E4D",
        color"#8C1941",
        color"#731536",
        color"#59102A"
      ],
      [
        color"#F8F0FC",
        color"#F3D9FA",
        color"#EEBEFA",
        color"#E599F7",
        color"#DA77F2",
        color"#CC5DE8",
        color"#BE4BDB",
        color"#AE3EC9",
        color"#9C36B5",
        color"#862E9C",
        color"#702682",
        color"#5A1E69",
        color"#44174F"
      ],
      [
        color"#F3F0FF",
        color"#E5DBFF",
        color"#D0BFFF",
        color"#B197FC",
        color"#9775FA",
        color"#845EF7",
        color"#7950F2",
        color"#7048E8",
        color"#6741D9",
        color"#5F3DC4",
        color"#5235AB",
        color"#462D91",
        color"#3A2578"
      ],
      [
        color"#EDF2FF",
        color"#DBE4FF",
        color"#BAC8FF",
        color"#91A7FF",
        color"#748FFC",
        color"#5C7CFA",
        color"#4C6EF5",
        color"#4263EB",
        color"#3B5BDB",
        color"#364FC7",
        color"#2F44AD",
        color"#283A94",
        color"#21307A"
      ],
      [
        color"#E7F5FF",
        color"#D0EBFF",
        color"#A5D8FF",
        color"#74C0FC",
        color"#4DABF7",
        color"#339AF0",
        color"#228BE6",
        color"#1C7ED6",
        color"#1971C2",
        color"#1864AB",
        color"#145591",
        color"#114678",
        color"#0D375E"
      ],
      [
        color"#E3FAFC",
        color"#C5F6FA",
        color"#99E9F2",
        color"#66D9E8",
        color"#3BC9DB",
        color"#22B8CF",
        color"#15AABF",
        color"#1098AD",
        color"#0C8599",
        color"#0B7285",
        color"#095C6B",
        color"#074652",
        color"#053038"
      ],
      [
        color"#E6FCF5",
        color"#C3FAE8",
        color"#96F2D7",
        color"#63E6BE",
        color"#38D9A9",
        color"#20C997",
        color"#12B886",
        color"#0CA678",
        color"#099268",
        color"#087F5B",
        color"#066649",
        color"#054D37",
        color"#033325"
      ],
      [
        color"#EBFBEE",
        color"#D3F9D8",
        color"#B2F2BB",
        color"#8CE99A",
        color"#69DB7C",
        color"#51CF66",
        color"#40C057",
        color"#37B24D",
        color"#2F9E44",
        color"#2B8A3E",
        color"#237032",
        color"#1B5727",
        color"#133D1B"
      ],
      [
        color"#F4FCE3",
        color"#E9FAC8",
        color"#D8F5A2",
        color"#C0EB75",
        color"#A9E34B",
        color"#94D82D",
        color"#82C91E",
        color"#74B816",
        color"#66A80F",
        color"#5C940D",
        color"#4C7A0B",
        color"#3C6109",
        color"#2C4706"
      ],
      [
        color"#FFF9DB",
        color"#FFF3BF",
        color"#FFEC99",
        color"#FFE066",
        color"#FFD43B",
        color"#FCC419",
        color"#FAB005",
        color"#F59F00",
        color"#F08C00",
        color"#E67700",
        color"#B35C00",
        color"#804200",
        color"#663500"
      ],
      [
        color"#FFF4E6",
        color"#FFE8CC",
        color"#FFD8A8",
        color"#FFC078",
        color"#FFA94D",
        color"#FF922B",
        color"#FD7E14",
        color"#F76707",
        color"#E8590C",
        color"#D9480F",
        color"#BF400D",
        color"#99330B",
        color"#802B09"
      ],
      [
        color"#FFF8DC",
        color"#FCE1BC",
        color"#F7CA9E",
        color"#F1B280",
        color"#E99B62",
        color"#DF8545",
        color"#D46E25",
        color"#BD5F1B",
        color"#A45117",
        color"#8A4513",
        color"#703A13",
        color"#572F12",
        color"#3D210D"
      ],
      [
        color"#FAF4EB",
        color"#EDE0D1",
        color"#E0CAB7",
        color"#D3B79E",
        color"#C5A285",
        color"#B78F6D",
        color"#A87C56",
        color"#956B47",
        color"#825B3A",
        color"#6F4B2D",
        color"#5E3A21",
        color"#4E2B15",
        color"#422412"
      ],
      [
        color"#F8FAFB",
        color"#E6E4DC",
        color"#D5CFBD",
        color"#C2B9A0",
        color"#AEA58C",
        color"#9A9178",
        color"#867C65",
        color"#736A53",
        color"#5F5746",
        color"#4B4639",
        color"#38352D",
        color"#252521",
        color"#121210"
      ],
      [
        color"#F9FBE7",
        color"#E8ED9C",
        color"#D2DF4E",
        color"#C2CE34",
        color"#B5BB2E",
        color"#A7A827",
        color"#999621",
        color"#8C851C",
        color"#7E7416",
        color"#6D6414",
        color"#5D5411",
        color"#4D460E",
        color"#36300A"
      ],
      [
        color"#ECFEB0",
        color"#DEF39A",
        color"#D0E884",
        color"#C2DD6E",
        color"#B5D15B",
        color"#A8C648",
        color"#9BBB36",
        color"#8FB024",
        color"#84A513",
        color"#7A9908",
        color"#658006",
        color"#516605",
        color"#3D4D04"
      ],
    )
  rgbBasic* = palette(
    [
      color"#FF0000",
      color"#FF5500",
      color"#FFAA00",
      color"#FFFF00",
      color"#AAFF00",
      color"#55FF00"
    ],
    [
      color"#FF0055",
      color"#00000000",
      color"#00000000",
      color"#00000000",
      color"#00000000",
      color"#00FF00"
    ],
    [
      color"#FF00AA",
      color"#000000",
      color"#555555",
      color"#AAAAAA",
      color"#FFFFFF",
      color"#00FF55"
    ],
    [
      color"#FF00FF",
      color"#000000AA",
      color"#00000055",
      color"#FFFFFF55",
      color"#FFFFFFAA",
      color"#00FFAA"
    ],
    [
      color"#AA00FF",
      color"#5500FF",
      color"#0000FF",
      color"#0055FF",
      color"#00AAFF",
      color"#00FFFF"
    ],
  )
  colar* = palette(
    [
      color"#F8FAFB",
      color"#F2F4F6",
      color"#EBEDEF",
      color"#E0E4E5",
      color"#D1D6D8",
      color"#B1B6B9",
      color"#979B9D",
      color"#7E8282",
      color"#666968",
      color"#50514F",
      color"#3A3A37",
      color"#252521",
      color"#121210"
    ],
    [
      color"#FFF5F5",
      color"#FFE3E3",
      color"#FFC9C9",
      color"#FFA8A8",
      color"#FF8787",
      color"#FF6B6B",
      color"#FA5252",
      color"#F03E3E",
      color"#E03131",
      color"#C92A2A",
      color"#B02525",
      color"#962020",
      color"#7D1A1A"
    ],
    [
      color"#FFF0F6",
      color"#FFDEEB",
      color"#FCC2D7",
      color"#FAA2C1",
      color"#F783AC",
      color"#F06595",
      color"#E64980",
      color"#D6336C",
      color"#C2255C",
      color"#A61E4D",
      color"#8C1941",
      color"#731536",
      color"#59102A"
    ],
    [
      color"#F8F0FC",
      color"#F3D9FA",
      color"#EEBEFA",
      color"#E599F7",
      color"#DA77F2",
      color"#CC5DE8",
      color"#BE4BDB",
      color"#AE3EC9",
      color"#9C36B5",
      color"#862E9C",
      color"#702682",
      color"#5A1E69",
      color"#44174F"
    ],
    [
      color"#F3F0FF",
      color"#E5DBFF",
      color"#D0BFFF",
      color"#B197FC",
      color"#9775FA",
      color"#845EF7",
      color"#7950F2",
      color"#7048E8",
      color"#6741D9",
      color"#5F3DC4",
      color"#5235AB",
      color"#462D91",
      color"#3A2578"
    ],
    [
      color"#EDF2FF",
      color"#DBE4FF",
      color"#BAC8FF",
      color"#91A7FF",
      color"#748FFC",
      color"#5C7CFA",
      color"#4C6EF5",
      color"#4263EB",
      color"#3B5BDB",
      color"#364FC7",
      color"#2F44AD",
      color"#283A94",
      color"#21307A"
    ],
    [
      color"#E7F5FF",
      color"#D0EBFF",
      color"#A5D8FF",
      color"#74C0FC",
      color"#4DABF7",
      color"#339AF0",
      color"#228BE6",
      color"#1C7ED6",
      color"#1971C2",
      color"#1864AB",
      color"#145591",
      color"#114678",
      color"#0D375E"
    ],
    [
      color"#E3FAFC",
      color"#C5F6FA",
      color"#99E9F2",
      color"#66D9E8",
      color"#3BC9DB",
      color"#22B8CF",
      color"#15AABF",
      color"#1098AD",
      color"#0C8599",
      color"#0B7285",
      color"#095C6B",
      color"#074652",
      color"#053038"
    ],
    [
      color"#E6FCF5",
      color"#C3FAE8",
      color"#96F2D7",
      color"#63E6BE",
      color"#38D9A9",
      color"#20C997",
      color"#12B886",
      color"#0CA678",
      color"#099268",
      color"#087F5B",
      color"#066649",
      color"#054D37",
      color"#033325"
    ],
    [
      color"#EBFBEE",
      color"#D3F9D8",
      color"#B2F2BB",
      color"#8CE99A",
      color"#69DB7C",
      color"#51CF66",
      color"#40C057",
      color"#37B24D",
      color"#2F9E44",
      color"#2B8A3E",
      color"#237032",
      color"#1B5727",
      color"#133D1B"
    ],
    [
      color"#F4FCE3",
      color"#E9FAC8",
      color"#D8F5A2",
      color"#C0EB75",
      color"#A9E34B",
      color"#94D82D",
      color"#82C91E",
      color"#74B816",
      color"#66A80F",
      color"#5C940D",
      color"#4C7A0B",
      color"#3C6109",
      color"#2C4706"
    ],
    [
      color"#FFF9DB",
      color"#FFF3BF",
      color"#FFEC99",
      color"#FFE066",
      color"#FFD43B",
      color"#FCC419",
      color"#FAB005",
      color"#F59F00",
      color"#F08C00",
      color"#E67700",
      color"#B35C00",
      color"#804200",
      color"#663500"
    ],
    [
      color"#FFF4E6",
      color"#FFE8CC",
      color"#FFD8A8",
      color"#FFC078",
      color"#FFA94D",
      color"#FF922B",
      color"#FD7E14",
      color"#F76707",
      color"#E8590C",
      color"#D9480F",
      color"#BF400D",
      color"#99330B",
      color"#802B09"
    ],
    [
      color"#FFF8DC",
      color"#FCE1BC",
      color"#F7CA9E",
      color"#F1B280",
      color"#E99B62",
      color"#DF8545",
      color"#D46E25",
      color"#BD5F1B",
      color"#A45117",
      color"#8A4513",
      color"#703A13",
      color"#572F12",
      color"#3D210D"
    ],
    [
      color"#FAF4EB",
      color"#EDE0D1",
      color"#E0CAB7",
      color"#D3B79E",
      color"#C5A285",
      color"#B78F6D",
      color"#A87C56",
      color"#956B47",
      color"#825B3A",
      color"#6F4B2D",
      color"#5E3A21",
      color"#4E2B15",
      color"#422412"
    ],
    [
      color"#F8FAFB",
      color"#E6E4DC",
      color"#D5CFBD",
      color"#C2B9A0",
      color"#AEA58C",
      color"#9A9178",
      color"#867C65",
      color"#736A53",
      color"#5F5746",
      color"#4B4639",
      color"#38352D",
      color"#252521",
      color"#121210"
    ],
    [
      color"#F9FBE7",
      color"#E8ED9C",
      color"#D2DF4E",
      color"#C2CE34",
      color"#B5BB2E",
      color"#A7A827",
      color"#999621",
      color"#8C851C",
      color"#7E7416",
      color"#6D6414",
      color"#5D5411",
      color"#4D460E",
      color"#36300A"
    ],
    [
      color"#ECFEB0",
      color"#DEF39A",
      color"#D0E884",
      color"#C2DD6E",
      color"#B5D15B",
      color"#A8C648",
      color"#9BBB36",
      color"#8FB024",
      color"#84A513",
      color"#7A9908",
      color"#658006",
      color"#516605",
      color"#3D4D04"
    ],
  )

M src/utils.nim => src/utils.nim +10 -10
@@ 8,18 8,18 @@ import std/strutils
proc color*(code: string): Color =
  if code.len == 7 and code[0] == '#':
    return Color(
        a: 255,
        r: code[1..2].parseHexInt().uint8,
        g: code[3..4].parseHexInt().uint8,
        b: code[5..6].parseHexInt().uint8,
      )
      a: 255,
      r: code[1 .. 2].parseHexInt().uint8,
      g: code[3 .. 4].parseHexInt().uint8,
      b: code[5 .. 6].parseHexInt().uint8,
    )
  elif code.len == 9 and code[0] == '#':
    return Color(
        a: code[7..8].parseHexInt().uint8,
        r: code[1..2].parseHexInt().uint8,
        g: code[3..4].parseHexInt().uint8,
        b: code[5..6].parseHexInt().uint8,
      )
      a: code[7 .. 8].parseHexInt().uint8,
      r: code[1 .. 2].parseHexInt().uint8,
      g: code[3 .. 4].parseHexInt().uint8,
      b: code[5 .. 6].parseHexInt().uint8,
    )
  else:
    raise newException(ValueError, "Invalid color: " & code)


M src/vier.nim => src/vier.nim +135 -167
@@ 83,7 83,7 @@ type
const
  backgroundColor0 = color"#555555"
  backgroundColor1 = color"#AAAAAA"
  codePoints = toSeq(32'i32..126'i32) & @["…".runeAt(0).int32]
  codePoints = toSeq(32'i32 .. 126'i32) & @["…".runeAt(0).int32]
  defaultScale = 8'i32
  fps = 60'i32
  letterWidth = 8


@@ 134,17 134,16 @@ proc newPicture(): Picture =

proc newPicture(fileName: string): Picture =
  ## Creates a picture by opening the given file.
  result =
    Picture(
      image:
        try:
          loadImage(fileName)
        except RaylibError:
          genImageColor(64, 64, Blank)
      ,
      fileName: fileName,
      scale: defaultScale,
    )
  result = Picture(
    image:
      try:
        loadImage(fileName)
      except RaylibError:
        genImageColor(64, 64, Blank)
    ,
    fileName: fileName,
    scale: defaultScale,
  )
  result.loadTexture()

proc canvasSize(picture: Picture): Vec =


@@ 166,8 165,9 @@ proc colorAtCursor(picture: Picture): Color =

proc moveCursor(picture: Picture, movement: Vec) =
  ## Moves the cursor by the given amount, ensuring that it stays in bounds.
  picture.cursor.x = clamp(picture.cursor.x + movement.x, 0'i32..<picture.image.width)
  picture.cursor.y = clamp(picture.cursor.y + movement.y, 0'i32..<picture.image.height)
  picture.cursor.x = clamp(picture.cursor.x + movement.x, 0'i32 ..< picture.image.width)
  picture.cursor.y =
    clamp(picture.cursor.y + movement.y, 0'i32 ..< picture.image.height)

proc add(picture: Picture, change: ImageChange) =
  ## Adds the change to the picture’s change history. Does *not* apply the change.


@@ 247,8 247,8 @@ proc lineSegment(start, `end`: Vec, includeStart: bool): seq[Vec] =

proc rectangleFilled(a, b: Vec): seq[Vec] =
  collect:
    for y in min(a.y, b.y)..max(a.y, b.y):
      for x in min(a.x, b.x)..max(a.x, b.x):
    for y in min(a.y, b.y) .. max(a.y, b.y):
      for x in min(a.x, b.x) .. max(a.x, b.x):
        (x, y)

proc rectangleOutline(a, b: Vec): seq[Vec] =


@@ 256,19 256,19 @@ proc rectangleOutline(a, b: Vec): seq[Vec] =
  let v = max(a, b)
  concat(
    collect(
      for x in u.x..v.x:
      for x in u.x .. v.x:
        (x, u.y)
    ),
    collect(
      for x in u.x..v.x:
      for x in u.x .. v.x:
        (x, v.y)
    ),
    collect(
      for y in u.y + 1..v.y - 1:
      for y in u.y + 1 .. v.y - 1:
        (u.x, y)
    ),
    collect(
      for y in u.y + 1..v.y - 1:
      for y in u.y + 1 .. v.y - 1:
        (v.x, y)
    ),
  )


@@ 287,8 287,9 @@ proc ellipse(a, b: Vec, filled: bool): seq[Vec] =
    parity = diameter.y and 1
    errorIncrementIncrement: Vec = (8 * diameter.y ^ 2, 8 * diameter.x ^ 2)
  var
    errorIncrement: Vec =
      (4 * (1 - diameter.x) * diameter.y ^ 2, 4 * (1 + parity) * diameter.x ^ 2)
    errorIncrement: Vec = (
      4 * (1 - diameter.x) * diameter.y ^ 2, 4 * (1 + parity) * diameter.x ^ 2
    )
    error = errorIncrement.x + errorIncrement.y + parity * diameter.x ^ 2
  a.y += (diameter.y + 1) div 2
  b.y = a.y - parity


@@ 360,34 361,28 @@ proc select(picture: Picture, target: Vec, tool: Tool) =

proc injectColor(picture: Picture, color: Color) =
  ## Changes all selected pixels to the given color.
  let
    change =
      ImageChange(
        pixelChanges:
          collect(
            for pixel in picture.selection:
              PixelChange(pos: pixel, originalColor: picture[pixel], newColor: color)
          )
      )
  let change = ImageChange(
    pixelChanges: collect(
      for pixel in picture.selection:
        PixelChange(pos: pixel, originalColor: picture[pixel], newColor: color)
    )
  )
  picture.add(change)
  picture.apply(change)

proc addColor(picture: Picture, color: Color) =
  ## Adds the given color to all selected pixels.
  let
    change =
      ImageChange(
        pixelChanges:
          collect(
            for pixel in picture.selection:
              let originalColor = picture[pixel]
              PixelChange(
                pos: pixel,
                originalColor: originalColor,
                newColor: colorAlphaBlend(originalColor, color, White),
              )
          )
      )
  let change = ImageChange(
    pixelChanges: collect(
      for pixel in picture.selection:
        let originalColor = picture[pixel]
        PixelChange(
          pos: pixel,
          originalColor: originalColor,
          newColor: colorAlphaBlend(originalColor, color, White),
        )
    )
  )
  picture.add(change)
  picture.apply(change)



@@ 446,17 441,17 @@ proc drawTextTruncated(
  else:
    var pos = pos.toVector2
    let half = (maxLen - 2) div 2
    let firstPart = text[0..<half]
    let firstPart = text[0 ..< half]
    drawText(font, firstPart.cstring, pos, textSize, textSpacing, color)
    pos.x += measureText(font, firstPart.cstring, textSize, textSpacing).x
    drawText(font, cstring"…", pos, textSize, textSpacing, ellipsisColor)
    pos.x += measureText(font, cstring"…", textSize, textSpacing).x
    let secondPart = text[^half..^1]
    let secondPart = text[^half ..^ 1]
    drawText(font, secondPart.cstring, pos, textSize, textSpacing, color)

proc drawBackground(origin: Vec, size: Vec) =
  drawRectangle(origin, size, backgroundColor0)
  for t in 0..<size.x + size.y:
  for t in 0 ..< size.x + size.y:
    if t mod 8 >= 4:
      let xOverlap = max(0, t - size.x)
      let yOverlap = max(0, t - size.y)


@@ 514,51 509,45 @@ iterator layout(
): (int, Picture, Vec) =
  ## Lays out the given pictures into the given rectangle and yields each picture along with its position.
  ## If the pictures exceed the allowed width, prefers to place the selection at the center of the container.
  let
    totalSize: Vec =
      (
        x:
          pictures.mapIt(it.widgetSize(isPalette = isPalette).x).sum() +
          (pictures.len.int32 - 1) * margin,
        y: pictures.mapIt(it.widgetSize(isPalette = isPalette).y).max(),
      )
  let totalSize: Vec = (
    x:
      pictures.mapIt(it.widgetSize(isPalette = isPalette).x).sum() +
      (pictures.len.int32 - 1) * margin,
    y: pictures.mapIt(it.widgetSize(isPalette = isPalette).y).max(),
  )
  let selectedPicture = pictures[selectedIndex]
  var
    origin =
      pos +
      (
        x: (
          if totalSize.x <= maxSize.x: 0'i32
  var origin =
    pos + (
      x: (
        if totalSize.x <= maxSize.x: 0'i32
        else:
          let center =
            pictures[0 ..< selectedIndex]
            .mapIt(it.widgetSize(isPalette = isPalette).x)
            .sum() + selectedIndex * margin +
            selectedPicture.scale * selectedPicture.cursor.x +
            selectedPicture.scale div 2
          let halfMax = maxSize.x div 2
          if center <= halfMax: 0
          elif totalSize.x - center <= halfMax:
            maxSize.x - totalSize.x
          else:
            let
              center =
                pictures[0..<selectedIndex].mapIt(
                  it.widgetSize(isPalette = isPalette).x
                ).sum() + selectedIndex * margin +
                selectedPicture.scale * selectedPicture.cursor.x +
                selectedPicture.scale div 2
            let halfMax = maxSize.x div 2
            if center <= halfMax: 0
            elif totalSize.x - center <= halfMax:
              maxSize.x - totalSize.x
            else:
              halfMax - center
        ),
        y: (
          if totalSize.y <= maxSize.y: 0'i32
            halfMax - center
      ),
      y: (
        if totalSize.y <= maxSize.y: 0'i32
        else:
          let center =
            selectedPicture.scale * selectedPicture.cursor.y +
            selectedPicture.scale div 2
          let halfMax = maxSize.y div 2
          if center <= halfMax: 0
          elif totalSize.y - center <= halfMax:
            maxSize.y - totalSize.y
          else:
            let
              center =
                selectedPicture.scale * selectedPicture.cursor.y +
                selectedPicture.scale div 2
            let halfMax = maxSize.y div 2
            if center <= halfMax: 0
            elif totalSize.y - center <= halfMax:
              maxSize.y - totalSize.y
            else:
              halfMax - center
        ),
      )
            halfMax - center
      ),
    )
  for index, picture in pictures:
    if origin.x + picture.widgetSize(isPalette = isPalette).x >= pos.x and
        origin.x <= pos.x + maxSize.x:


@@ 596,28 585,24 @@ proc copy(app: App) =
  let selection = app.selectedPicture.selection
  let xMin = selection.mapIt(it.x).min()
  let yMin = selection.mapIt(it.y).min()
  app.clipboard =
    collect:
      for pixel in selection:
        PixelChange(pos: pixel - (xMin, yMin), newColor: app.selectedPicture[pixel])
  app.clipboard = collect:
    for pixel in selection:
      PixelChange(pos: pixel - (xMin, yMin), newColor: app.selectedPicture[pixel])

proc paste(app: App) =
  let picture = app.selectedPicture
  let cursor = picture.cursor
  let
    change =
      ImageChange(
        pixelChanges:
          collect(
            for pixel in app.clipboard:
              if cursor + pixel.pos < picture.image.size:
                PixelChange(
                  pos: cursor + pixel.pos,
                  originalColor: picture[pixel.pos],
                  newColor: pixel.newColor,
                )
  let change = ImageChange(
    pixelChanges: collect(
      for pixel in app.clipboard:
        if cursor + pixel.pos < picture.image.size:
          PixelChange(
            pos: cursor + pixel.pos,
            originalColor: picture[pixel.pos],
            newColor: pixel.newColor,
          )
      )
    )
  )
  picture.add(change)
  picture.apply(change)



@@ 724,18 709,14 @@ proc processKeyboard(app: App) =
      app.mode = Color
    if isKeyPressed(D):
      let picture = app.selectedPicture
      let
        change =
          ImageChange(
            pixelChanges:
              @[
                PixelChange(
                  pos: picture.cursor,
                  originalColor: picture.colorAtCursor,
                  newColor: Blank,
                )
              ]
          )
      let change = ImageChange(
        pixelChanges:
          @[
            PixelChange(
              pos: picture.cursor, originalColor: picture.colorAtCursor, newColor: Blank
            )
          ]
      )
      picture.add(change)
      picture.apply(change)
    if isKeyPressed(E):


@@ 798,10 779,9 @@ proc processKeyboard(app: App) =
        app.pictures.apply(zoomOut)
      else:
        app.selectedPicture.zoomOut()
    var
      movement =
        int32(isKeyDown(H)) * (-1'i32, 0'i32) + int32(isKeyDown(J)) * (0'i32, 1'i32) +
        int32(isKeyDown(K)) * (0'i32, -1'i32) + int32(isKeyDown(L)) * (1'i32, 0'i32)
    var movement =
      int32(isKeyDown(H)) * (-1'i32, 0'i32) + int32(isKeyDown(J)) * (0'i32, 1'i32) +
      int32(isKeyDown(K)) * (0'i32, -1'i32) + int32(isKeyDown(L)) * (1'i32, 0'i32)
    if movement == square(0):
      app.movementTime = 0
    else:


@@ 813,7 793,7 @@ proc processKeyboard(app: App) =
      if movement != square(0):
        if control:
          app.selectedPictureIndex =
            clamp(app.selectedPictureIndex + movement.x, 0..app.pictures.high)
            clamp(app.selectedPictureIndex + movement.x, 0 .. app.pictures.high)
        else:
          app.selectedPicture.moveCursor(movement)
          if isKeyDown(Space) and not isKeyPressed(Space):


@@ 821,28 801,20 @@ proc processKeyboard(app: App) =
              app.selectedPicture.cursor,
              app.mode,
              app.tool,
              if shift:
                app.secondaryColor
              else:
                app.color
              ,
              if shift: app.secondaryColor else: app.color,
            )
      if isKeyPressed(Space):
        app.selectedPicture.clickPixel(
          app.selectedPicture.cursor,
          app.mode,
          app.tool,
          if shift:
            app.secondaryColor
          else:
            app.color
          ,
          if shift: app.secondaryColor else: app.color,
        )
    of Color:
      if movement != square(0):
        if control:
          app.selectedPaletteIndex =
            clamp(app.selectedPaletteIndex + movement.x, 0..app.palettes.high)
            clamp(app.selectedPaletteIndex + movement.x, 0 .. app.palettes.high)
          app.updateColor()
        else:
          app.selectedPalette.moveCursor(movement)


@@ 916,26 888,24 @@ proc addApi(spry: var spryvm.Interpreter, app: App) =
  ## Adds functions for interacting with the editor to the Spry virtual machine.
  nimFunc("edit"):
    let fileNameNode = spry.evalArg()
    let
      fileName =
        if fileNameNode of StringVal:
          fileNameNode.StringVal.value
        else:
          raiseRuntimeException("File name not a string: " & $fileNameNode)
          ""
    let fileName =
      if fileNameNode of StringVal:
        fileNameNode.StringVal.value
      else:
        raiseRuntimeException("File name not a string: " & $fileNameNode)
        ""
    app.pictures.add(newPicture(fileName))
    app.selectedPictureIndex = app.pictures.high
  nimFunc("e"):
    let fileNameNode = spry.arg()
    let
      fileName =
        if fileNameNode of Word:
          fileNameNode.Word.word
        elif fileNameNode of StringVal:
          fileNameNode.StringVal.value
        else:
          raiseRuntimeException("File name not a word or string: " & $fileNameNode)
          ""
    let fileName =
      if fileNameNode of Word:
        fileNameNode.Word.word
      elif fileNameNode of StringVal:
        fileNameNode.StringVal.value
      else:
        raiseRuntimeException("File name not a word or string: " & $fileNameNode)
        ""
    app.pictures.add(newPicture(fileName))
    app.selectedPictureIndex = app.pictures.high
  nimFunc("flip-x"):


@@ 969,26 939,24 @@ proc addApi(spry: var spryvm.Interpreter, app: App) =
      raiseRuntimeException("Dimensions must be integers")
  nimFunc("write"):
    let fileNameNode = spry.evalArg()
    let
      fileName =
        if fileNameNode of StringVal:
          fileNameNode.StringVal.value
        else:
          raiseRuntimeException("File name not a string: " & $fileNameNode)
          ""
    let fileName =
      if fileNameNode of StringVal:
        fileNameNode.StringVal.value
      else:
        raiseRuntimeException("File name not a string: " & $fileNameNode)
        ""
    app.selectedPicture.fileName = fileName
    app.selectedPicture.write()
  nimFunc("w"):
    let fileNameNode = spry.arg()
    let
      fileName =
        if fileNameNode of Word:
          fileNameNode.Word.word
        elif fileNameNode of StringVal:
          fileNameNode.StringVal.value
        else:
          raiseRuntimeException("File name not a word or string: " & $fileNameNode)
          ""
    let fileName =
      if fileNameNode of Word:
        fileNameNode.Word.word
      elif fileNameNode of StringVal:
        fileNameNode.StringVal.value
      else:
        raiseRuntimeException("File name not a word or string: " & $fileNameNode)
        ""
    app.selectedPicture.fileName = fileName
    app.selectedPicture.write()