about summary refs log tree commit diff stats
path: root/081print.mu
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2017-05-18 09:44:37 -0700
committerKartik K. Agaram <vc@akkartik.com>2017-05-18 09:57:57 -0700
commitee1a18f050a5458ade460720091e20ce6b335011 (patch)
treece16273f91254d3d966a1f5427bf9af04b4f136a /081print.mu
parent7f67383400216732166ff8c845829b93b217ff30 (diff)
downloadmu-ee1a18f050a5458ade460720091e20ce6b335011.tar.gz
3860 - stop buffering the screen in termbox
To achieve this we have to switch to a model of the screen in termbox that
is closer to the underlying terminal.

Before:
  a screen is a grid of characters
  writing out of bounds does nothing

After:
  a screen is a scrolling raster of characters
  writing out of bounds wraps to next line and scrolls if necessary

To move to the new model, it was essential that I migrate my fake screen
at the same time to mimic it. This is why the first attempt (commit 3824)
failed (commit 3858). This is also why this commit can't be split into
smaller pieces.

The fake screen now 'scrolls' by rotating screen lines from top to bottom.
There's still no notion of a scrollback buffer.

The newer model is richer; it permits repl-like apps that upstream termbox
can't do easily. It also permits us to simply use `printf` or `cout` to
write to the screen, and everything mostly works as you would expect. Exceptions:

  a) '\n' won't do what you expect. You need to explicitly print both '\n'
  and '\r'.

  b) backspace won't do what you expect. It only moves the cursor back,
  without erasing the previous character. It does not wrap.

  Both behaviors exactly mimic my existing terminal's emulation of vt100.

The catch: it's easy to accidentally scroll in apps. Out-of-bounds prints
didn't matter before, but they're bugs now. To help track them down, use
the `save-top-idx`, `assert-no-scroll` pair of helpers.

  An important trick is to wrap the cursor before rather after printing
  a character. Otherwise we end up scrolling every time we print to the
  bottom-right character. This means that the cursor position can be invalid
  at the start of a print, and we need to handle that.

In the process we also lose the ability to hide and show the screen. We
have to show the prints happening. Seems apt for a "white-box" platform
like Mu.
Diffstat (limited to '081print.mu')
-rw-r--r--081print.mu304
1 files changed, 217 insertions, 87 deletions
diff --git a/081print.mu b/081print.mu
index b778948e..2c2e09ae 100644
--- a/081print.mu
+++ b/081print.mu
@@ -6,7 +6,9 @@ container screen [
   num-columns:num
   cursor-row:num
   cursor-column:num
-  data:&:@:screen-cell
+  data:&:@:screen-cell  # capacity num-rows*num-columns
+  top-idx:num  # index inside data that corresponds to top-left of screen
+               # modified on scroll, wrapping around to the top of data
 ]
 
 container screen-cell [
@@ -18,9 +20,13 @@ def new-fake-screen w:num, h:num -> result:&:screen [
   local-scope
   load-ingredients
   result <- new screen:type
+  non-zero-width?:bool <- greater-than w, 0
+  assert non-zero-width?, [screen can't have zero width]
+  non-zero-height?:bool <- greater-than h, 0
+  assert non-zero-height?, [screen can't have zero height]
   bufsize:num <- multiply w, h
   data:&:@:screen-cell <- new screen-cell:type, bufsize
-  *result <- merge h/num-rows, w/num-columns, 0/cursor-row, 0/cursor-column, data
+  *result <- merge h/num-rows, w/num-columns, 0/cursor-row, 0/cursor-column, data, 0/top-idx
   result <- clear-screen result
 ]
 
@@ -48,6 +54,7 @@ def clear-screen screen:&:screen -> screen:&:screen [
   # reset cursor
   *screen <- put *screen, cursor-row:offset, 0
   *screen <- put *screen, cursor-column:offset, 0
+  *screen <- put *screen, top-idx:offset, 0
 ]
 
 def fake-screen-is-empty? screen:&:screen -> result:bool [
@@ -97,71 +104,152 @@ def print screen:&:screen, c:char -> screen:&:screen [
   # (handle special cases exactly like in the real screen)
   width:num <- get *screen, num-columns:offset
   height:num <- get *screen, num-rows:offset
-  # if cursor is out of bounds, silently exit
+  capacity:num <- multiply width, height
   row:num <- get *screen, cursor-row:offset
-  row <- round row
-  legal?:bool <- greater-or-equal row, 0
-  return-unless legal?
-  legal? <- lesser-than row, height
-  return-unless legal?
   column:num <- get *screen, cursor-column:offset
+  buf:&:@:screen-cell <- get *screen, data:offset
+  # some potentially slow sanity checks for preconditions {
+  # eliminate fractions from column and row
+  row <- round row
   column <- round column
-  legal? <- greater-or-equal column, 0
-  return-unless legal?
-  legal? <- lesser-than column, width
-  return-unless legal?
+  # if cursor is past left margin (error), reset to left margin
+  {
+    too-far-left?:bool <- lesser-than column, 0
+    break-unless too-far-left?
+    column <- copy 0
+    *screen <- put *screen, cursor-column:offset, column
+  }
+  # if cursor is at right margin, wrap
+  {
+    at-right?:bool <- equal column, width
+    break-unless at-right?
+    column <- copy 0
+    *screen <- put *screen, cursor-column:offset, column
+    row <- add row, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  # if cursor is past right margin (error), reset to right margin
+  {
+    too-far-right?:bool <- greater-than column, width
+    break-unless too-far-right?
+    column <- subtract width, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  # if row is above top margin (error), reset to top margin
+  {
+    too-far-up?:bool <- lesser-than row, 0
+    break-unless too-far-up?
+    row <- copy 0
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  # if row is at bottom margin, scroll
+  {
+    at-bottom?:bool <- equal row, height
+    break-unless at-bottom?
+    scroll-fake-screen screen
+    row <- subtract height, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  # if row is below bottom margin (error), reset to bottom margin
+  {
+    too-far-down?:bool <- greater-than row, height
+    break-unless too-far-down?
+    row <- subtract height, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  # }
 #?     $print [print-character (], row, [, ], column, [): ], c, 10/newline
   # special-case: newline
   {
     newline?:bool <- equal c, 10/newline
     break-unless newline?
-    {
-      # unless cursor is already at bottom
-      bottom:num <- subtract height, 1
-      at-bottom?:bool <- greater-or-equal row, bottom
-      break-if at-bottom?
-      # move it to the next row
-      column <- copy 0
-      *screen <- put *screen, cursor-column:offset, column
-      row <- add row, 1
-      *screen <- put *screen, cursor-row:offset, row
-    }
+    cursor-down-on-fake-screen screen  # doesn't modify column
+    return
+  }
+  # special-case: linefeed
+  {
+    linefeed?:bool <- equal c, 13/linefeed
+    break-unless linefeed?
+    *screen <- put *screen, cursor-column:offset, 0
     return
   }
-  # save character in fake screen
-  index:num <- multiply row, width
-  index <- add index, column
-  buf:&:@:screen-cell <- get *screen, data:offset
-  len:num <- length *buf
   # special-case: backspace
+  # moves cursor left but does not erase
   {
-    backspace?:bool <- equal c, 8
+    backspace?:bool <- equal c, 8/backspace
     break-unless backspace?
     {
-      # unless cursor is already at left margin
-      at-left?:bool <- lesser-or-equal column, 0
-      break-if at-left?
-      # clear previous location
+      break-unless column
       column <- subtract column, 1
       *screen <- put *screen, cursor-column:offset, column
-      index <- subtract index, 1
-      cursor:screen-cell <- merge 32/space, 7/white
-      *buf <- put-index *buf, index, cursor
     }
     return
   }
+  # save character in fake screen
+  top-idx:num <- get *screen, top-idx:offset
+  index:num <- data-index row, column, width, height, top-idx
   cursor:screen-cell <- merge c, color
   *buf <- put-index *buf, index, cursor
-  # increment column unless it's already all the way to the right
+  # move cursor to next character
+  # (but don't bother making it valid; we'll do that before the next print)
+  column <- add column, 1
+  *screen <- put *screen, cursor-column:offset, column
+]
+
+def cursor-down-on-fake-screen screen:&:screen -> screen:&:screen [
+  local-scope
+  load-ingredients
+  row:num <- get *screen, cursor-row:offset
+  height:num <- get *screen, num-rows:offset
+  bottom:num <- subtract height, 1
+  at-bottom?:bool <- greater-or-equal row, bottom
   {
-    right:num <- subtract width, 1
-    at-right?:bool <- greater-or-equal column, right
-    break-if at-right?
-    column <- add column, 1
-    *screen <- put *screen, cursor-column:offset, column
+    break-if at-bottom?
+    row <- add row, 1
+    *screen <- put *screen, cursor-row:offset, row
+  }
+  {
+    break-unless at-bottom?
+    scroll-fake-screen screen  # does not modify row
   }
 ]
 
+def scroll-fake-screen screen:&:screen -> screen:&:screen [
+  local-scope
+  load-ingredients
+  width:num <- get *screen, num-columns:offset
+  height:num <- get *screen, num-rows:offset
+  buf:&:@:screen-cell <- get *screen, data:offset
+  # clear top line and 'rotate' it to the bottom
+  top-idx:num <- get *screen, top-idx:offset  # 0 <= top-idx < len(buf)
+  next-top-idx:num <- add top-idx, width  # 0 <= next-top-idx <= len(buf)
+  empty-cell:screen-cell <- merge 0, 0
+  {
+    done?:bool <- greater-or-equal top-idx, next-top-idx
+    break-if done?
+    put-index *buf, top-idx, empty-cell
+    top-idx <- add top-idx, 1
+    # no modulo; top-idx is always a multiple of width,
+    # so it can never wrap around inside this loop
+    loop
+  }
+  # top-idx now same as next-top-idx; wrap around if necessary
+  capacity:num <- multiply width, height
+  _, top-idx <- divide-with-remainder, top-idx, capacity
+  *screen <- put *screen, top-idx:offset, top-idx
+]
+
+# translate from screen (row, column) coordinates to an index into data
+# while accounting for scrolling (sliding top-idx)
+def data-index row:num, column:num, width:num, height:num, top-idx:num -> result:num [
+  local-scope
+  load-ingredients
+  result <- multiply width, row
+  result <- add result, column, top-idx
+  capacity:num <- multiply width, height
+  _, result <- divide-with-remainder result, capacity
+]
+
 scenario print-character-at-top-left [
   local-scope
   fake-screen:&:screen <- new-fake-screen 3/width, 2/height
@@ -232,7 +320,7 @@ scenario print-backspace-character [
   memory-should-contain [
     10 <- 0  # cursor column
     11 <- 6  # width*height
-    12 <- 32  # space, not 'a'
+    12 <- 97  # still 'a'
     13 <- 7  # white
     # rest of screen is empty
     14 <- 0
@@ -255,7 +343,7 @@ scenario print-extra-backspace-character [
   memory-should-contain [
     1 <- 0  # cursor column
     3 <- 6  # width*height
-    4 <- 32  # space, not 'a'
+    4 <- 97  # still 'a'
     5 <- 7  # white
     # rest of screen is empty
     6 <- 0
@@ -271,22 +359,26 @@ scenario print-character-at-right-margin [
   b:char <- copy 98/b
   fake-screen <- print fake-screen, b
   run [
-    # cursor now at right margin
+    # cursor now at next row
     c:char <- copy 99/c
     fake-screen <- print fake-screen, c
-    10:num/raw <- get *fake-screen, cursor-column:offset
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
     cell:&:@:screen-cell <- get *fake-screen, data:offset
-    11:@:screen-cell/raw <- copy *cell
+    12:@:screen-cell/raw <- copy *cell
   ]
   memory-should-contain [
-    10 <- 1  # cursor column
-    11 <- 4  # width*height
-    12 <- 97  # 'a'
-    13 <- 7  # white
-    14 <- 99  # 'c' over 'b'
-    15 <- 7  # white
-    # rest of screen is empty
-    16 <- 0
+    10 <- 1  # cursor row
+    11 <- 1  # cursor column
+    12 <- 4  # width*height
+    13 <- 97  # 'a'
+    14 <- 7  # white
+    15 <- 98  # 'b'
+    16 <- 7  # white
+    17 <- 99  # 'c'
+    18 <- 7  # white
+    19 <- 0  # ' '
+    20 <- 7  # white
   ]
 ]
 
@@ -305,7 +397,7 @@ scenario print-newline-character [
   ]
   memory-should-contain [
     10 <- 1  # cursor row
-    11 <- 0  # cursor column
+    11 <- 1  # cursor column
     12 <- 6  # width*height
     13 <- 97  # 'a'
     14 <- 7  # white
@@ -336,39 +428,81 @@ scenario print-newline-at-bottom-line [
 scenario print-character-at-bottom-right [
   local-scope
   fake-screen:&:screen <- new-fake-screen 2/width, 2/height
-  newline:char <- copy 10/newline
-  fake-screen <- print fake-screen, newline
   a:char <- copy 97/a
   fake-screen <- print fake-screen, a
   b:char <- copy 98/b
   fake-screen <- print fake-screen, b
   c:char <- copy 99/c
   fake-screen <- print fake-screen, c
-  fake-screen <- print fake-screen, newline
   run [
     # cursor now at bottom right
     d:char <- copy 100/d
     fake-screen <- print fake-screen, d
     10:num/raw <- get *fake-screen, cursor-row:offset
     11:num/raw <- get *fake-screen, cursor-column:offset
+    12:num/raw <- get *fake-screen, top-idx:offset
     cell:&:@:screen-cell <- get *fake-screen, data:offset
     20:@:screen-cell/raw <- copy *cell
   ]
+  # cursor column overflows the screen but is not wrapped yet
   memory-should-contain [
     10 <- 1  # cursor row
-    11 <- 1  # cursor column
-    20 <- 4  # width*height
-    21 <- 0  # unused
+    11 <- 2  # cursor column -- outside screen
+    12 <- 0  # top-idx -- not yet scrolled
+    20 <- 4  # screen size (width*height)
+    21 <- 97  # 'a'
     22 <- 7  # white
-    23 <- 0  # unused
+    23 <- 98  # 'b'
     24 <- 7  # white
-    25 <- 97 # 'a'
+    25 <- 99 # 'c'
     26 <- 7  # white
-    27 <- 100  # 'd' over 'b' and 'c' and newline
+    27 <- 100  # 'd'
     28 <- 7  # white
-    # rest of screen is empty
-    29 <- 0
   ]
+  run [
+    e:char <- copy 101/e
+    print fake-screen, e
+    10:num/raw <- get *fake-screen, cursor-row:offset
+    11:num/raw <- get *fake-screen, cursor-column:offset
+    12:num/raw <- get *fake-screen, top-idx:offset
+    cell:&:@:screen-cell <- get *fake-screen, data:offset
+    20:@:screen-cell/raw <- copy *cell
+  ]
+  memory-should-contain [
+    # text scrolls by 1, we lose the top line
+    10 <- 1  # cursor row
+    11 <- 1  # cursor column -- wrapped
+    12 <- 2  # top-idx -- scrolled
+    20 <- 4  # screen size (width*height)
+    # screen now checked in rotated order
+    25 <- 99 # 'c'
+    26 <- 7  # white
+    27 <- 100  # 'd'
+    28 <- 7  # white
+    # screen wraps; bottom line is cleared of old contents
+    21 <- 101  # 'e'
+    22 <- 7  # white
+    23 <- 0  # unused
+    24 <- 0  # no color
+  ]
+]
+
+# even though our screen supports scrolling, some apps may want to avoid
+# scrolling
+# these helpers help check for scrolling at development time
+def save-top-idx screen:&:screen -> result:num [
+  local-scope
+  load-ingredients
+  return-unless screen, 0  # check is only for fake screens
+  result <- get *screen, top-idx:offset
+]
+def assert-no-scroll screen:&:screen, old-top-idx:num [
+  local-scope
+  load-ingredients
+  return-unless screen
+  new-top-idx:num <- get *screen, top-idx:offset
+  no-scroll?:bool <- equal old-top-idx, new-top-idx
+  assert no-scroll?, [render should never use screen's scrolling capabilities]
 ]
 
 def clear-line screen:&:screen -> screen:&:screen [
@@ -398,10 +532,14 @@ def clear-line screen:&:screen -> screen:&:screen [
   *screen <- put *screen, cursor-column:offset, original-column
 ]
 
+# only for non-scrolling apps
 def clear-line-until screen:&:screen, right:num/inclusive -> screen:&:screen [
   local-scope
   load-ingredients
-  _, column:num <- cursor-position screen
+  row:num, column:num <- cursor-position screen
+  height:num <- screen-height screen
+  past-bottom?:bool <- greater-or-equal row, height
+  return-if past-bottom?
   space:char <- copy 32/space
   bg-color:num, bg-color-found?:bool <- next-ingredient
   {
@@ -597,22 +735,6 @@ def screen-height screen:&:screen -> height:num [
   height <- display-height
 ]
 
-def hide-screen screen:&:screen -> screen:&:screen [
-  local-scope
-  load-ingredients
-  return-if screen  # fake screen; do nothing
-  # real screen
-  hide-display
-]
-
-def show-screen screen:&:screen -> screen:&:screen [
-  local-scope
-  load-ingredients
-  return-if screen  # fake screen; do nothing
-  # real screen
-  show-display
-]
-
 def print screen:&:screen, s:text -> screen:&:screen [
   local-scope
   load-ingredients
@@ -640,24 +762,32 @@ def print screen:&:screen, s:text -> screen:&:screen [
   }
 ]
 
-scenario print-text-stops-at-right-margin [
+scenario print-text-wraps-past-right-margin [
   local-scope
   fake-screen:&:screen <- new-fake-screen 3/width, 2/height
   run [
     fake-screen <- print fake-screen, [abcd]
+    5:num/raw <- get *fake-screen, cursor-row:offset
+    6:num/raw <- get *fake-screen, cursor-column:offset
+    7:num/raw <- get *fake-screen, top-idx:offset
     cell:&:@:screen-cell <- get *fake-screen, data:offset
     10:@:screen-cell/raw <- copy *cell
   ]
   memory-should-contain [
+    5 <- 1  # cursor-row
+    6 <- 1  # cursor-column
+    7 <- 0  # top-idx
     10 <- 6  # width*height
     11 <- 97  # 'a'
     12 <- 7  # white
     13 <- 98  # 'b'
     14 <- 7  # white
-    15 <- 100  # 'd' overwrites 'c'
+    15 <- 99  # 'c'
     16 <- 7  # white
+    17 <- 100  # 'd'
+    18 <- 7  # white
     # rest of screen is empty
-    17 <- 0
+    19 <- 0
   ]
 ]