about summary refs log blame commit diff stats
path: root/500text-screen.mu
blob: 92ea1d2fddf7b625c64f48074c962998ddd50a90 (plain) (tree)
1
2
3
4
5
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
## handling events from the keyboard, mouse, touch screen, ...

# temporary main: interactive editor
# hit ctrl-c to exit
def! main text:text [
  local-scope
  load-ingredients
  open-console
  editor:address:editor-data <- new-editor text, 0/screen, 5/left, 45/right
  editor-event-loop 0/screen, 0/console, editor
  close-console
]

def editor-event-loop screen:address:screen, console:address:console, editor:address:editor-data -> screen:address:screen, console:address:console, editor:address:editor-data [
  local-scope
  load-ingredients
  {
    # looping over each (keyboard or touch) event as it occurs
    +next-event
    cursor-row:number <- get *editor, cursor-row:offset
    cursor-column:number <- get *editor, cursor-column:offset
    screen <- move-cursor screen, cursor-row, cursor-column
    e:event, console:address:console, found?:boolean, quit?:boolean <- read-event console
    loop-unless found?
    break-if quit?  # only in tests
    trace 10, [app], [next-event]
    # 'touch' event
    t:touch-event, is-touch?:boolean <- maybe-convert e, touch:variant
    {
      break-unless is-touch?
      move-cursor-in-editor screen, editor, t
      loop +next-event:label
    }
    # keyboard events
    {
      break-if is-touch?
      screen, editor, go-render?:boolean <- handle-keyboard-event screen, editor, e
      {
        break-unless go-render?
        screen <- editor-render screen, editor
      }
    }
    pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# Testable primitives for writing text to screen.
# (Mu doesn't yet have testable primitives for graphics.)
#
# Unlike the top-level, this text mode has no scrolling.

# coordinates here don't match top-level
# Here we're consistent with graphics mode. Top-level is consistent with
# terminal emulators.
type screen {
  width: int
  height: int
  data: (handle array screen-cell)
  cursor-x: int
  cursor-y: int
}

type screen-cell {
  data: grapheme
  color: int
  background-color: int
}

fn initialize-screen screen: (addr screen), width: int, height: int {
  var screen-addr/esi: (addr screen) <- copy screen
  var tmp/eax: int <- copy 0
  var dest/edi: (addr int) <- copy 0
  # screen->width = width
  dest <- get screen-addr, width
  tmp <- copy width
  copy-to *dest, tmp
  # screen->height = height
  dest <- get screen-addr, height
  tmp <- copy height
  copy-to *dest, tmp
  # screen->data = new screen-cell[width*height]
  {
    var data-addr/edi: (addr handle array screen-cell) <- get screen-addr, data
    tmp <- multiply width
    populate data-addr, tmp
  }
  # screen->cursor-x = 0
  dest <- get screen-addr, cursor-x
  copy-to *dest, 0
  # screen->cursor-y = 0
  dest <- get screen-addr, cursor-y
  copy-to *dest, 0
}

# in graphemes
fn screen-size screen: (addr screen) -> _/eax: int, _/ecx: int {
  var width/eax: int <- copy 0
  var height/ecx: int <- copy 0
  compare screen, 0
  {
    break-if-!=
    return 0x80/128, 0x30/48
  }
  # fake screen
  var screen-addr/esi: (addr screen) <- copy screen
  var tmp/edx: (addr int) <- get screen-addr, width
  width <- copy *tmp
  tmp <- get screen-addr, height
  height <- copy *tmp
  return width, height
}

# testable screen primitive
fn draw-grapheme screen: (addr screen), g: grapheme, x: int, y: int, color: int, background-color: int {
  {
    compare screen, 0
    break-if-!=
    draw-grapheme-on-real-screen g, x, y, color, background-color
    return
  }
  # fake screen
  var screen-addr/esi: (addr screen) <- copy screen
  var idx/ecx: int <- screen-cell-index screen-addr, x, y
  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
  var data/eax: (addr array screen-cell) <- lookup *data-ah
  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
  var dest-cell/ecx: (addr screen-cell) <- index data, offset
  var dest-grapheme/eax: (addr grapheme) <- get dest-cell, data
  var g2/edx: grapheme <- copy g
  copy-to *dest-grapheme, g2
  var dest-color/eax: (addr int) <- get dest-cell, color
  var src-color/edx: int <- copy color
  copy-to *dest-color, src-color
  dest-color <- get dest-cell, background-color
  src-color <- copy background-color
  copy-to *dest-color, src-color
}

# we can't really render non-ASCII yet, but when we do we'll be ready
fn draw-code-point screen: (addr screen), c: code-point, x: int, y: int, color: int, background-color: int {
  var g/eax: grapheme <- copy c
  draw-grapheme screen, g, x, y, color, background-color
}

# not really needed for a real screen, though it shouldn't do any harm
fn screen-cell-index screen-on-stack: (addr screen), x: int, y: int -> _/ecx: int {
  var screen/esi: (addr screen) <- copy screen-on-stack
  # only one bounds check isn't automatically handled
  {
    var xmax/eax: (addr int) <- get screen, width
    var xcurr/ecx: int <- copy x
    compare xcurr, *xmax
    break-if-<
    abort "tried to print out of screen bounds"
  }
  var width-addr/eax: (addr int) <- get screen, width
  var result/ecx: int <- copy y
  result <- multiply *width-addr
  result <- add x
  return result
}

fn cursor-position screen: (addr screen) -> _/eax: int, _/ecx: int {
  {
    compare screen, 0
    break-if-!=
    var x/eax: int <- copy 0
    var y/ecx: int <- copy 0
    x, y <- cursor-position-on-real-screen
    return x, y
  }
  # fake screen
  var screen-addr/esi: (addr screen) <- copy screen
  var cursor-x-addr/eax: (addr int) <- get screen-addr, cursor-x
  var cursor-y-addr/ecx: (addr int) <- get screen-addr, cursor-y
  return *cursor-x-addr, *cursor-y-addr
}

fn set-cursor-position screen: (addr screen), x: int, y: int {
  {
    compare screen, 0
    break-if-!=
    set-cursor-position-on-real-screen x, y
    return
  }
  # fake screen
  var screen-addr/esi: (addr screen) <- copy screen
  # ignore x < 0
  {
    compare x, 0
    break-if->=
    return
  }
  # ignore x >= width
  {
    var width-addr/eax: (addr int) <- get screen-addr, width
    var width/eax: int <- copy *width-addr
    compare x, width
    break-if-<=
    return
  }
  # ignore y < 0
  {
    compare y, 0
    break-if->=
    return
  }
  # ignore y >= height
  {
    var height-addr/eax: (addr int) <- get screen-addr, height
    var height/eax: int <- copy *height-addr
    compare y, height
    break-if-<
    return
  }
  # screen->cursor-x = x
  var dest/edi: (addr int) <- get screen-addr, cursor-x
  var src/eax: int <- copy x
  copy-to *dest, src
  # screen->cursor-y = y
  dest <- get screen-addr, cursor-y
  src <- copy y
  copy-to *dest, src
}

fn draw-cursor screen: (addr screen), g: grapheme {
  {
    compare screen, 0
    break-if-!=
    draw-cursor-on-real-screen g
    return
  }
  # fake screen
  var cursor-x/eax: int <- copy 0
  var cursor-y/ecx: int <- copy 0
  cursor-x, cursor-y <- cursor-position screen
  draw-grapheme screen, g, cursor-x, cursor-y, 0/fg, 7/bg
}

fn clear-screen screen: (addr screen) {
  {
    compare screen, 0
    break-if-!=
    clear-real-screen
    return
  }
  # fake screen
  set-cursor-position screen, 0, 0
  var screen-addr/esi: (addr screen) <- copy screen
  var y/eax: int <- copy 0
  var height/ecx: (addr int) <- get screen-addr, height
  {
    compare y, *height
    break-if->=
    var x/edx: int <- copy 0
    var width/ebx: (addr int) <- get screen-addr, width
    {
      compare x, *width
      break-if->=
      draw-code-point screen, 0x20/space, x, y, 0/fg=black, 0/bg=black
      x <- increment
      loop
    }
    y <- increment
    loop
  }
  set-cursor-position screen, 0, 0
}

fn clear-rect screen: (addr screen), xmin: int, ymin: int, xmax: int, ymax: int, background-color: int {
  {
    compare screen, 0
    break-if-!=
    clear-rect-on-real-screen xmin, ymin, xmax, ymax, background-color
    return
  }
  # fake screen
  set-cursor-position screen, 0, 0
  var screen-addr/esi: (addr screen) <- copy screen
  var y/eax: int <- copy ymin
  var ymax/ecx: int <- copy ymax
  {
    compare y, ymax
    break-if->=
    var x/edx: int <- copy xmin
    var xmax/ebx: int <- copy xmax
    {
      compare x, xmax
      break-if->=
      draw-code-point screen, 0x20/space, x, y, 0/fg, background-color
      x <- increment
      loop
    }
    y <- increment
    loop
  }
  set-cursor-position screen, 0, 0
}

# there's no grapheme that guarantees to cover every pixel, so we'll bump down
# to pixels for a real screen
fn clear-real-screen {
  var y/eax: int <- copy 0
  {
    compare y, 0x300/screen-height=768
    break-if->=
    var x/edx: int <- copy 0
    {
      compare x, 0x400/screen-width=1024
      break-if->=
      pixel-on-real-screen x, y, 0/color=black
      x <- increment
      loop
    }
    y <- increment
    loop
  }
}

fn clear-rect-on-real-screen xmin: int, ymin: int, xmax: int, ymax: int, background-color: int {
  var y/eax: int <- copy ymin
  y <- shift-left 4/log-font-height
  var ymax/ecx: int <- copy ymax
  ymax <- shift-left 4/log-font-height
  {
    compare y, ymax
    break-if->=
    var x/edx: int <- copy xmin
    x <- shift-left 3/log-font-width
    var xmax/ebx: int <- copy xmax
    xmax <- shift-left 3/log-font-width
    {
      compare x, xmax
      break-if->=
      pixel-on-real-screen x, y, background-color
      x <- increment
      loop
    }
    y <- increment
    loop
  }
}

fn screen-grapheme-at screen-on-stack: (addr screen), x: int, y: int -> _/eax: grapheme {
  var screen-addr/esi: (addr screen) <- copy screen-on-stack
  var idx/ecx: int <- screen-cell-index screen-addr, x, y
  var result/eax: grapheme <- screen-grapheme-at-idx screen-addr, idx
  return result
}

fn screen-grapheme-at-idx screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: grapheme {
  var screen-addr/esi: (addr screen) <- copy screen-on-stack
  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
  var data/eax: (addr array screen-cell) <- lookup *data-ah
  var idx/ecx: int <- copy idx-on-stack
  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
  var cell/eax: (addr screen-cell) <- index data, offset
  var src/eax: (addr grapheme) <- get cell, data
  return *src
}

fn screen-color-at screen-on-stack: (addr screen), x: int, y: int -> _/eax: int {
  var screen-addr/esi: (addr screen) <- copy screen-on-stack
  var idx/ecx: int <- screen-cell-index screen-addr, x, y
  var result/eax: int <- screen-color-at-idx screen-addr, idx
  return result
}

fn screen-color-at-idx screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: int {
  var screen-addr/esi: (addr screen) <- copy screen-on-stack
  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
  var data/eax: (addr array screen-cell) <- lookup *data-ah
  var idx/ecx: int <- copy idx-on-stack
  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
  var cell/eax: (addr screen-cell) <- index data, offset
  var src/eax: (addr int) <- get cell, color
  var result/eax: int <- copy *src
  return result
}

fn screen-background-color-at screen-on-stack: (addr screen), x: int, y: int -> _/eax: int {
  var screen-addr/esi: (addr screen) <- copy screen-on-stack
  var idx/ecx: int <- screen-cell-index screen-addr, x, y
  var result/eax: int <- screen-background-color-at-idx screen-addr, idx
  return result
}

fn screen-background-color-at-idx screen-on-stack: (addr screen), idx-on-stack: int -> _/eax: int {
  var screen-addr/esi: (addr screen) <- copy screen-on-stack
  var data-ah/eax: (addr handle array screen-cell) <- get screen-addr, data
  var data/eax: (addr array screen-cell) <- lookup *data-ah
  var idx/ecx: int <- copy idx-on-stack
  var offset/ecx: (offset screen-cell) <- compute-offset data, idx
  var cell/eax: (addr screen-cell) <- index data, offset
  var src/eax: (addr int) <- get cell, background-color
  var result/eax: int <- copy *src
  return result
}
>column [ assume-screen 10/width, 5/height 1:text <- new [abc] # editor occupies only left half of screen 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ # click on right half of screen left-click 3, 8 ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] screen-should-contain [ . . .abc . .┈┈┈┈┈ . . . ] memory-should-contain [ 3 <- 1 # no change to cursor row 4 <- 0 # ..or column ] check-trace-count-for-label 0, [print-character] ] scenario editor-handles-mouse-clicks-in-menu-area [ assume-screen 10/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ # click on first, 'menu' row left-click 0, 3 ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] # no change to cursor memory-should-contain [ 3 <- 1 4 <- 0 ] ] scenario editor-inserts-characters-into-empty-editor [ assume-screen 10/width, 5/height 1:text <- new [] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ type [abc] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .abc . .┈┈┈┈┈ . . . ] check-trace-count-for-label 3, [print-character] ] scenario editor-inserts-characters-at-cursor [ assume-screen 10/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right editor-render screen, 2:address:editor-data $clear-trace # type two letters at different places assume-console [ type [0] left-click 1, 2 type [d] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .0adbc . .┈┈┈┈┈┈┈┈┈┈. . . ] check-trace-count-for-label 7, [print-character] # 4 for first letter, 3 for second ] scenario editor-inserts-characters-at-cursor-2 [ assume-screen 10/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ left-click 1, 5 # right of last line type [d] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .abcd . .┈┈┈┈┈┈┈┈┈┈. . . ] check-trace-count-for-label 1, [print-character] ] scenario editor-inserts-characters-at-cursor-5 [ assume-screen 10/width, 5/height 1:text <- new [abc d] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ left-click 1, 5 # right of non-last line type [e] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .abce . .d . .┈┈┈┈┈┈┈┈┈┈. . . ] check-trace-count-for-label 1, [print-character] ] scenario editor-inserts-characters-at-cursor-3 [ assume-screen 10/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ left-click 3, 5 # below all text type [d] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .abcd . .┈┈┈┈┈┈┈┈┈┈. . . ] check-trace-count-for-label 1, [print-character] ] scenario editor-inserts-characters-at-cursor-4 [ assume-screen 10/width, 5/height 1:text <- new [abc d] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ left-click 3, 5 # below all text type [e] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .abc . .de . .┈┈┈┈┈┈┈┈┈┈. . . ] check-trace-count-for-label 1, [print-character] ] scenario editor-inserts-characters-at-cursor-6 [ assume-screen 10/width, 5/height 1:text <- new [abc d] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right editor-render screen, 2:address:editor-data $clear-trace assume-console [ left-click 3, 5 # below all text type [ef] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .abc . .def . .┈┈┈┈┈┈┈┈┈┈. . . ] check-trace-count-for-label 2, [print-character] ] scenario editor-moves-cursor-after-inserting-characters [ assume-screen 10/width, 5/height 1:text <- new [ab] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right editor-render screen, 2:address:editor-data assume-console [ type [01] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .01ab . .┈┈┈┈┈ . . . ] ] # if the cursor reaches the right margin, wrap the line scenario editor-wraps-line-on-insert [ assume-screen 5/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right editor-render screen, 2:address:editor-data # type a letter assume-console [ type [e] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] # no wrap yet screen-should-contain [ . . .eabc . .┈┈┈┈┈. . . . . ] # type a second letter assume-console [ type [f] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] # now wrap screen-should-contain [ . . .efab. .c . .┈┈┈┈┈. . . ] ] scenario editor-wraps-line-on-insert-2 [ # create an editor with some text assume-screen 10/width, 5/height 1:text <- new [abcdefg defg] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right editor-render screen, 2:address:editor-data # type more text at the start assume-console [ left-click 3, 0 type [abc] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] # cursor is not wrapped memory-should-contain [ 3 <- 3 4 <- 3 ] # but line is wrapped screen-should-contain [ . . .abcd . .efg . .abcd . .efg . ] ] after <insert-character-special-case> [ # if the line wraps at the cursor, move cursor to start of next row { # if either: # a) we're at the end of the line and at the column of the wrap indicator, or # b) we're not at end of line and just before the column of the wrap indicator wrap-column:number <- copy right before-wrap-column:number <- subtract wrap-column, 1 at-wrap?:boolean <- greater-or-equal cursor-column, wrap-column just-before-wrap?:boolean <- greater-or-equal cursor-column, before-wrap-column next:address:duplex-list:character <- next before-cursor # at end of line? next == 0 || next.value == 10/newline at-end-of-line?:boolean <- equal next, 0 { break-if at-end-of-line? next-character:character <- get *next, value:offset at-end-of-line? <- equal next-character, 10/newline } # break unless ((eol? and at-wrap?) or (~eol? and just-before-wrap?)) move-cursor-to-next-line?:boolean <- copy 0/false { break-if at-end-of-line? move-cursor-to-next-line? <- copy just-before-wrap? # if we're moving the cursor because it's in the middle of a wrapping # line, adjust it to left-most column potential-new-cursor-column:number <- copy left } { break-unless at-end-of-line? move-cursor-to-next-line? <- copy at-wrap? # if we're moving the cursor because it's at the end of a wrapping line, # adjust it to one past the left-most column to make room for the # newly-inserted wrap-indicator potential-new-cursor-column:number <- add left, 1/make-room-for-wrap-indicator } break-unless move-cursor-to-next-line? cursor-column <- copy potential-new-cursor-column *editor <- put *editor, cursor-column:offset, cursor-column cursor-row <- add cursor-row, 1 *editor <- put *editor, cursor-row:offset, cursor-row # if we're out of the screen, scroll down { below-screen?:boolean <- greater-or-equal cursor-row, screen-height break-unless below-screen? <scroll-down> } go-render? <- copy 1/true return } ] scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [ assume-screen 10/width, 5/height 1:text <- new [abcde] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right assume-console [ left-click 1, 3 # right before the wrap icon type [f] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] screen-should-contain [ . . .abcf . .de . .┈┈┈┈┈ . . . ] memory-should-contain [ 3 <- 2 # cursor row 4 <- 0 # cursor column ] ] scenario editor-wraps-cursor-after-inserting-characters-at-end-of-line [ local-scope assume-screen 10/width, 5/height # create an editor containing two lines contents:text <- new [abc xyz] 1:address:editor-data/raw <- new-editor contents, screen, 0/left, 5/right screen-should-contain [ . . .abc . .xyz . . . ] assume-console [ left-click 1, 4 # at end of first line type [de] # trigger wrap ] run [ editor-event-loop screen:address:screen, console:address:console, 1:address:editor-data/raw ] screen-should-contain [ . . .abcd . .e . .xyz . .┈┈┈┈┈ . ] ] scenario editor-wraps-cursor-to-left-margin [ assume-screen 10/width, 5/height 1:text <- new [abcde] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 2/left, 7/right assume-console [ left-click 1, 5 # line is full; no wrap icon yet type [01] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] screen-should-contain [ . . . abc0 . . 1de . . ┈┈┈┈┈ . . . ] memory-should-contain [ 3 <- 2 # cursor row 4 <- 3 # cursor column ] ] # if newline, move cursor to start of next line, and maybe align indent with previous line container editor-data [ indent?:boolean ] after <editor-initialization> [ *result <- put *result, indent?:offset, 1/true ] scenario editor-moves-cursor-down-after-inserting-newline [ assume-screen 10/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right assume-console [ type [0 1] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . .0 . .1abc . .┈┈┈┈┈┈┈┈┈┈. . . ] ] after <handle-special-character> [ { newline?:boolean <- equal c, 10/newline break-unless newline? <insert-enter-begin> editor <- insert-new-line-and-indent editor, screen <insert-enter-end> go-render? <- copy 1/true return } ] def insert-new-line-and-indent editor:address:editor-data, screen:address:screen -> editor:address:editor-data, screen:address:screen, go-render?:boolean [ local-scope load-ingredients cursor-row:number <- get *editor, cursor-row:offset cursor-column:number <- get *editor, cursor-column:offset before-cursor:address:duplex-list:character <- get *editor, before-cursor:offset left:number <- get *editor, left:offset right:number <- get *editor, right:offset screen-height:number <- screen-height screen # insert newline insert 10/newline, before-cursor before-cursor <- next before-cursor *editor <- put *editor, before-cursor:offset, before-cursor cursor-row <- add cursor-row, 1 *editor <- put *editor, cursor-row:offset, cursor-row cursor-column <- copy left *editor <- put *editor, cursor-column:offset, cursor-column # maybe scroll { below-screen?:boolean <- greater-or-equal cursor-row, screen-height # must be equal, never greater break-unless below-screen? <scroll-down> go-render? <- copy 1/true cursor-row <- subtract cursor-row, 1 # bring back into screen range *editor <- put *editor, cursor-row:offset, cursor-row } # indent if necessary indent?:boolean <- get *editor, indent?:offset return-unless indent? d:address:duplex-list:character <- get *editor, data:offset end-of-previous-line:address:duplex-list:character <- prev before-cursor indent:number <- line-indent end-of-previous-line, d i:number <- copy 0 { indent-done?:boolean <- greater-or-equal i, indent break-if indent-done? editor, screen, go-render?:boolean <- insert-at-cursor editor, 32/space, screen i <- add i, 1 loop } ] # takes a pointer 'curr' into the doubly-linked list and its sentinel, counts # the number of spaces at the start of the line containing 'curr'. def line-indent curr:address:duplex-list:character, start:address:duplex-list:character -> result:number [ local-scope load-ingredients result:number <- copy 0 return-unless curr at-start?:boolean <- equal curr, start return-if at-start? { curr <- prev curr break-unless curr at-start?:boolean <- equal curr, start break-if at-start? c:character <- get *curr, value:offset at-newline?:boolean <- equal c, 10/newline break-if at-newline? # if c is a space, increment result is-space?:boolean <- equal c, 32/space { break-unless is-space? result <- add result, 1 } # if c is not a space, reset result { break-if is-space? result <- copy 0 } loop } ] scenario editor-moves-cursor-down-after-inserting-newline-2 [ assume-screen 10/width, 5/height 1:text <- new [abc] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 1/left, 10/right assume-console [ type [0 1] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] screen-should-contain [ . . . 0 . . 1abc . . ┈┈┈┈┈┈┈┈┈. . . ] ] scenario editor-clears-previous-line-completely-after-inserting-newline [ assume-screen 10/width, 5/height 1:text <- new [abcde] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 5/right assume-console [ press enter ] screen-should-contain [ . . .abcd . .e . . . . . ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data ] # line should be fully cleared screen-should-contain [ . . . . .abcd . .e . .┈┈┈┈┈ . ] ] scenario editor-inserts-indent-after-newline [ assume-screen 10/width, 10/height 1:text <- new [ab cd ef] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right # position cursor after 'cd' and hit 'newline' assume-console [ left-click 2, 8 type [ ] ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] # cursor should be below start of previous line memory-should-contain [ 3 <- 3 # cursor row 4 <- 2 # cursor column (indented) ] ] scenario editor-skips-indent-around-paste [ assume-screen 10/width, 10/height 1:text <- new [ab cd ef] 2:address:editor-data <- new-editor 1:text, screen:address:screen, 0/left, 10/right # position cursor after 'cd' and hit 'newline' surrounded by paste markers assume-console [ left-click 2, 8 press 65507 # start paste press enter press 65506 # end paste ] run [ editor-event-loop screen:address:screen, console:address:console, 2:address:editor-data 3:number <- get *2:address:editor-data, cursor-row:offset 4:number <- get *2:address:editor-data, cursor-column:offset ] # cursor should be below start of previous line memory-should-contain [ 3 <- 3 # cursor row 4 <- 0 # cursor column (not indented) ] ] after <handle-special-key> [ { paste-start?:boolean <- equal k, 65507/paste-start break-unless paste-start? *editor <- put *editor, indent?:offset, 0/false go-render? <- copy 1/true return } ] after <handle-special-key> [ { paste-end?:boolean <- equal k, 65506/paste-end break-unless paste-end? *editor <- put *editor, indent?:offset, 1/true go-render? <- copy 1/true return } ] ## helpers def draw-horizontal screen:address:screen, row:number, x:number, right:number -> screen:address:screen [ local-scope load-ingredients style:character, style-found?:boolean <- next-ingredient { break-if style-found? style <- copy 9472/horizontal } color:number, color-found?:boolean <- next-ingredient { # default color to white break-if color-found? color <- copy 245/grey } bg-color:number, bg-color-found?:boolean <- next-ingredient { break-if bg-color-found? bg-color <- copy 0/black } screen <- move-cursor screen, row, x { continue?:boolean <- lesser-or-equal x, right # right is inclusive, to match editor-data semantics break-unless continue? print screen, style, color, bg-color x <- add x, 1 loop } ]