## handling events from the keyboard, mouse, touch screen, ... # temporary main: interactive editor # hit ctrl-c to exit def! main text:text [ local-scope load-inputs open-console clear-screen null/screen # non-scrolling app editor:&:editor <- new-editor text, 5/left, 45/right editor-render null/screen, editor editor-event-loop null/screen, null/console, editor close-console ] def editor-event-loop screen:&:screen, console:&:console, editor:&:editor -> screen:&:screen, console:&:console, editor:&:editor [ local-scope load-inputs { # looping over each (keyboard or touch) event as it occurs +next-event cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset screen <- move-cursor screen, cursor-row, cursor-column e:event, found?:bool, quit?:bool, console <- read-event console loop-unless found? break-if quit? # only in tests trace 10, [app], [next-event] # 'touch' event t:touch-event, is-touch?:bool <- maybe-convert e, touch:variant { break-unless is-touch? move-cursor editor, screen, t loop +next-event } # keyboard events { break-if is-touch? go-render?:bool <- handle-keyboard-event screen, editor, e { break-unless go-render? screen <- editor-render screen, editor } } loop } ] # process click, return if it was on current editor def move-cursor editor:&:editor, screen:&:screen, t:touch-event -> in-focus?:bool, editor:&:editor [ local-scope load-inputs return-unless editor, false click-row:num <- get t, row:offset return-unless click-row, false # ignore clicks on 'menu' click-column:num <- get t, column:offset left:num <- get *editor, left:offset too-far-left?:bool <- lesser-than click-column, left return-if too-far-left?, false right:num <- get *editor, right:offset too-far-right?:bool <- greater-than click-column, right return-if too-far-right?, false # position cursor editor <- snap-cursor editor, screen, click-row, click-column undo-coalesce-tag:num <- copy 0/never # gain focus return true ] # Variant of 'render' that only moves the cursor (coordinates and # before-cursor). If it's past the end of a line, it 'slides' it left. If it's # past the last line it positions at end of last line. def snap-cursor editor:&:editor, screen:&:screen, target-row:num, target-column:num -> editor:&:editor [ local-scope load-inputs return-unless editor left:num <- get *editor, left:offset right:num <- get *editor, right:offset screen-height:num <- screen-height screen # count newlines until screen row curr:&:duplex-list:char <- get *editor, top-of-screen:offset prev:&:duplex-list:char <- copy curr # just in case curr becomes null and we can't compute prev curr <- next curr row:num <- copy 1/top column:num <- copy left *editor <- put *editor, cursor-row:offset, target-row cursor-row:num <- copy target-row *editor <- put *editor, cursor-column:offset, target-column cursor-column:num <- copy target-column before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset { +next-character break-unless curr off-screen?:bool <- greater-or-equal row, screen-height break-if off-screen? # update editor.before-cursor # Doing so at the start of each iteration ensures it stays one step behind # the current character. { at-cursor-row?:bool <- equal row, cursor-row break-unless at-cursor-row? at-cursor?:bool <- equal column, cursor-column break-unless at-cursor? before-cursor <- copy prev *editor <- put *editor, before-cursor:offset, before-cursor } c:char <- get *curr, value:offset { # newline? move to left rather than 0 newline?:bool <- equal c, 10/newline break-unless newline? # adjust cursor if necessary { at-cursor-row?:bool <- equal row, cursor-row break-unless at-cursor-row? left-of-cursor?:bool <- lesser-than column, cursor-column break-unless left-of-cursor? cursor-column <- copy column *editor <- put *editor, cursor-column:offset, cursor-column before-cursor <- copy prev *editor <- put *editor, before-cursor:offset, before-cursor } # skip to next line row <- add row, 1 column <- copy left curr <- next curr prev <- next prev loop +next-character } { # at right? wrap. even if there's only one more letter left; we need # room for clicking on the cursor after it. at-right?:bool <- equal column, right break-unless at-right? column <- copy left row <- add row, 1 # don't increment curr/prev loop +next-character } curr <- next curr prev <- next prev column <- add column, 1 loop } # is cursor to the right of the last line? move to end { at-cursor-row?:bool <- equal row, cursor-row cursor-outside-line?:bool <- lesser-or-equal column, cursor-column before-cursor-on-same-line?:bool <- and at-cursor-row?, cursor-outside-line? above-cursor-row?:bool <- lesser-than row, cursor-r
# tests for 'scenario' in previous layer

scenario first_scenario_in_mu [
  run [
    1:number <- add 2, 2
  ]
  memory-should-contain [
    1 <- 4
  ]
]

scenario scenario_with_comment_in_mu [
  run [
    # comment
    1:number <- add 2, 2
  ]
  memory-should-contain [
    1 <- 4
  ]
]

scenario scenario_with_multiple_comments_in_mu [
  run [
    # comment1
    # comment2
    1:number <- add 2, 2
  ]
  memory-should-contain [
    1 <- 4
  ]
]

scenario check_string_in_memory [
  run [
    1:number <- copy 3
    2:character <- copy 97  # 'a'
    3:character <- copy 98  # 'b'
    4:character <- copy 99  # 'c'
  ]
  memory-should-contain [
    1:string <- [abc]
  ]
]

scenario check_trace [
  run [
    1:number <- add 2, 2
  ]
  trace-should-contain [
    mem: storing 4 in location 1
  ]
]

scenario check_trace_negative [
  run [
    1:number <- add 2, 2
  ]
  trace-should-not-contain [
    mem: storing 5 in location 1
  ]
]

scenario check_trace_instruction [
  run [
    trace 1, [foo], [aaa]
  ]
  trace-should-contain [
    foo: aaa
  ]
]
-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?:bool <- greater-or-equal cursor-row, screen-height break-unless below-screen? } return true/go-render } ] scenario editor-wraps-cursor-after-inserting-characters-in-middle-of-line [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abcde], 0/left, 5/right assume-console [ left-click 1, 3 # right before the wrap icon type [f] ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, 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 s:text <- new [abc xyz] e:&:editor <- new-editor s, 0/left, 5/right editor-render screen, e 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, console, e ] screen-should-contain [ . . .abcd↩ . .e . .xyz . .┈┈┈┈┈ . ] ] scenario editor-wraps-cursor-to-left-margin [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abcde], 2/left, 7/right assume-console [ left-click 1, 5 # line is full; no wrap icon yet type [01] ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, 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 [ indent?:bool ] after [ *result <- put *result, indent?:offset, true ] scenario editor-moves-cursor-down-after-inserting-newline [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abc], 0/left, 10/right assume-console [ type [0 1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . .0 . .1abc . .┈┈┈┈┈┈┈┈┈┈. . . ] ] after [ { newline?:bool <- equal c, 10/newline break-unless newline? insert-new-line-and-indent editor, screen return true/go-render } ] def insert-new-line-and-indent editor:&:editor, screen:&:screen -> editor:&:editor, screen:&:screen [ local-scope load-inputs cursor-row:num <- get *editor, cursor-row:offset cursor-column:num <- get *editor, cursor-column:offset before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset left:num <- get *editor, left:offset right:num <- get *editor, right:offset screen-height:num <- screen-height screen # update cursor coordinates at-start-of-wrapped-line?:bool <- at-start-of-wrapped-line? editor { break-if at-start-of-wrapped-line? 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?:bool <- greater-or-equal cursor-row, screen-height # must be equal, never greater break-unless below-screen? cursor-row <- subtract cursor-row, 1 # bring back into screen range *editor <- put *editor, cursor-row:offset, cursor-row } # insert newline insert 10/newline, before-cursor before-cursor <- next before-cursor *editor <- put *editor, before-cursor:offset, before-cursor # indent if necessary indent?:bool <- get *editor, indent?:offset return-unless indent? d:&:duplex-list:char <- get *editor, data:offset end-of-previous-line:&:duplex-list:char <- prev before-cursor indent:num <- line-indent end-of-previous-line, d i:num <- copy 0 { indent-done?:bool <- greater-or-equal i, indent break-if indent-done? insert-at-cursor editor, 32/space, screen i <- add i, 1 loop } ] def at-start-of-wrapped-line? editor:&:editor -> result:bool [ local-scope load-inputs left:num <- get *editor, left:offset cursor-column:num <- get *editor, cursor-column:offset cursor-at-left?:bool <- equal cursor-column, left return-unless cursor-at-left?, false before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset before-before-cursor:&:duplex-list:char <- prev before-cursor return-unless before-before-cursor, false # cursor is at start of editor char-before-cursor:char <- get *before-cursor, value:offset cursor-after-newline?:bool <- equal char-before-cursor, 10/newline return-if cursor-after-newline?, false # if cursor is at left margin and not at start, but previous character is not a newline, # then we're at start of a wrapped line return true ] # 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:&:duplex-list:char, start:&:duplex-list:char -> result:num [ local-scope load-inputs result:num <- copy 0 return-unless curr at-start?:bool <- equal curr, start return-if at-start? { curr <- prev curr break-unless curr at-start?:bool <- equal curr, start break-if at-start? c:char <- get *curr, value:offset at-newline?:bool <- equal c, 10/newline break-if at-newline? # if c is a space, increment result is-space?:bool <- 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 [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abc], 1/left, 10/right assume-console [ type [0 1] ] run [ editor-event-loop screen, console, e ] screen-should-contain [ . . . 0 . . 1abc . . ┈┈┈┈┈┈┈┈┈. . . ] ] scenario editor-clears-previous-line-completely-after-inserting-newline [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abcde], 0/left, 5/right editor-render screen, e screen-should-contain [ . . .abcd↩ . .e . .┈┈┈┈┈ . . . ] assume-console [ press enter ] run [ editor-event-loop screen, console, e ] # line should be fully cleared screen-should-contain [ . . . . .abcd↩ . .e . .┈┈┈┈┈ . ] ] scenario editor-splits-wrapped-line-after-inserting-newline [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abcdef], 0/left, 5/right editor-render screen, e screen-should-contain [ . . .abcd↩ . .ef . .┈┈┈┈┈ . . . ] assume-console [ left-click 2, 0 press enter ] run [ editor-event-loop screen, console, e 10:num/raw <- get *e, cursor-row:offset 11:num/raw <- get *e, cursor-column:offset ] screen-should-contain [ . . .abcd . .ef . .┈┈┈┈┈ . ] memory-should-contain [ 10 <- 2 # cursor-row 11 <- 0 # cursor-column ] ] scenario editor-inserts-indent-after-newline [ local-scope assume-screen 10/width, 10/height s:text <- new [ab cd ef] e:&:editor <- new-editor s, 0/left, 10/right # position cursor after 'cd' and hit 'newline' assume-console [ left-click 2, 8 type [ ] ] run [ editor-event-loop screen, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, 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 [ local-scope assume-screen 10/width, 10/height s:text <- new [ab cd ef] e:&:editor <- new-editor s, 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, console, e 3:num/raw <- get *e, cursor-row:offset 4:num/raw <- get *e, 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 [ { paste-start?:bool <- equal k, 65507/paste-start break-unless paste-start? *editor <- put *editor, indent?:offset, false return true/go-render } ] after [ { paste-end?:bool <- equal k, 65506/paste-end break-unless paste-end? *editor <- put *editor, indent?:offset, true return true/go-render } ] ## helpers def draw-horizontal screen:&:screen, row:num, x:num, right:num -> screen:&:screen [ local-scope load-inputs height:num <- screen-height screen past-bottom?:bool <- greater-or-equal row, height return-if past-bottom? style:char, style-found?:bool <- next-input { break-if style-found? style <- copy 9472/horizontal } color:num, color-found?:bool <- next-input { # default color to white break-if color-found? color <- copy 245/grey } bg-color:num, bg-color-found?:bool <- next-input { break-if bg-color-found? bg-color <- copy 0/black } screen <- move-cursor screen, row, x { continue?:bool <- lesser-or-equal x, right # right is inclusive, to match editor semantics break-unless continue? print screen, style, color, bg-color x <- add x, 1 loop } ]