1 ## putting the environment together out of editors
  2 #
  3 # Consists of one editor on the left for recipes and one on the right for the
  4 # sandbox.
  5 
  6 def! main [
  7   local-scope
  8   open-console
  9   env:&:environment <- new-programming-environment 0/filesystem, 0/screen
 10   render-all 0/screen, env, render
 11   event-loop 0/screen, 0/console, env, 0/filesystem
 12   # never gets here
 13 ]
 14 
 15 container environment [
 16   recipes:&:editor
 17   current-sandbox:&:editor
 18   sandbox-in-focus?:bool  # false => cursor in recipes; true => cursor in current-sandbox
 19 ]
 20 
 21 def new-programming-environment resources:&:resources, screen:&:screen, test-sandbox-editor-contents:text -> result:&:environment [
 22   local-scope
 23   load-ingredients
 24   width:num <- screen-width screen
 25   result <- new environment:type
 26   # recipe editor on the left
 27   initial-recipe-contents:text <- slurp resources, [lesson/recipes.mu]  # ignore errors
 28   divider:num, _ <- divide-with-remainder width, 2
 29   recipes:&:editor <- new-editor initial-recipe-contents, 0/left, divider/right
 30   # sandbox editor on the right
 31   sandbox-left:num <- add divider, 1
 32   current-sandbox:&:editor <- new-editor test-sandbox-editor-contents, sandbox-left, width/right
 33   *result <- put *result, recipes:offset, recipes
 34   *result <- put *result, current-sandbox:offset, current-sandbox
 35   *result <- put *result, sandbox-in-focus?:offset, 0/false
 36   <programming-environment-initialization>
 37 ]
 38 
 39 def event-loop screen:&:screen, console:&:console, env:&:environment, resources:&:resources -> screen:&:screen, console:&:console, env:&:environment, resources:&:resources [
 40   local-scope
 41   load-ingredients
 42   recipes:&:editor <- get *env, recipes:offset
 43   current-sandbox:&:editor <- get *env, current-sandbox:offset
 44   sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
 45   # if we fall behind we'll stop updating the screen, but then we have to
 46   # render the entire screen when we catch up.
 47   # todo: test this
 48   render-all-on-no-more-events?:bool <- copy 0/false
 49   {
 50     # looping over each (keyboard or touch) event as it occurs
 51     +next-event
 52     e:event, found?:bool, quit?:bool, console <- read-event console
 53     loop-unless found?
 54     break-if quit?  # only in tests
 55     trace 10, [app], [next-event]
 56     <handle-event>
 57     # check for global events that will trigger regardless of which editor has focus
 58     {
 59       k:num, is-keycode?:bool <- maybe-convert e:event, keycode:variant
 60       break-unless is-keycode?
 61       <global-keypress>
 62     }
 63     {
 64       c:char, is-unicode?:bool <- maybe-convert e:event, text:variant
 65       break-unless is-unicode?
 66       <global-type>
 67     }
 68     # 'touch' event - send to both sides, see what picks it up
 69     {
 70       t:touch-event, is-touch?:bool <- maybe-convert e:event, touch:variant
 71       break-unless is-touch?
 72       # ignore all but 'left-click' events for now
 73       # todo: test this
 74       touch-type:num <- get t, type:offset
 75       is-left-click?:bool <- equal touch-type, 65513/mouse-left
 76       loop-unless is-left-click?, +next-event
 77       click-row:num <- get t, row:offset
 78       click-column:num <- get t, column:offset
 79       # later exceptions for non-editor touches will go here
 80       <global-touch>
 81       # send to both editors
 82       _ <- move-cursor-in-editor screen, recipes, t
 83       sandbox-in-focus?:bool <- move-cursor-in-editor screen, current-sandbox, t
 84       *env <- put *env, sandbox-in-focus?:offset, sandbox-in-focus?
 85       screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
 86       loop +next-event
 87     }
 88     # 'resize' event - redraw editor
 89     # todo: test this after supporting resize in assume-console
 90     {
 91       r:resize-event, is-resize?:bool <- maybe-convert e:event, resize:variant
 92       break-unless is-resize?
 93       # if more events, we're still resizing; wait until we stop
 94       more-events?:bool <- has-more-events? console
 95       {
 96         break-unless more-events?
 97         render-all-on-no-more-events? <- copy 1/true  # no rendering now, full rendering on some future event
 98       }
 99       {
100         break-if more-events?
101         env, screen <- resize screen, env
102         screen <- render-all screen, env, render-without-moving-cursor
103         render-all-on-no-more-events? <- copy 0/false  # full render done
104       }
105       loop +next-event
106     }
107     # if it's not global and not a touch event, send to appropriate editor
108     {
109       hide-screen screen
110       sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
111       {
112         break-if sandbox-in-focus?
113         render?:bool <- handle-keyboard-event screen, recipes, e:event
114         # refresh screen only if no more events
115         # if there are more events to process, wait for them to clear up, then make sure you render-all afterward.
116         more-events?:bool <- has-more-events? console
117         {
118           break-unless more-events?
119           render-all-on-no-more-events? <- copy 1/true  # no rendering now, full rendering on some future event
120           jump +finish-event
121         }
122         {
123           break-if more-events?
124           {
125             break-unless render-all-on-no-more-events?
126             # no more events, and we have to force render
127             screen <- render-all screen, env, render
128             render-all-on-no-more-events? <- copy 0/false
129             jump +finish-event
130           }
131           # no more events, no force render
132           {
133             break-unless render?
134             screen <- render-recipes screen, env, render
135             jump +finish-event
136           }
137         }
138       }
139       {
140         break-unless sandbox-in-focus?
141         render?:bool <- handle-keyboard-event screen, current-sandbox, e:event
142         # refresh screen only if no more events
143         # if there are more events to process, wait for them to clear up, then make sure you render-all afterward.
144         more-events?:bool <- has-more-events? console
145         {
146           break-unless more-events?
147           render-all-on-no-more-events? <- copy 1/true  # no rendering now, full rendering on some future event
148           jump +finish-event
149         }
150         {
151           break-if more-events?
152           {
153             break-unless render-all-on-no-more-events?
154             # no more events, and we have to force render
155             screen <- render-all screen, env, render
156             render-all-on-no-more-events? <- copy 0/false
157             jump +finish-event
158           }
159           # no more events, no force render
160           {
161             break-unless render?
162             screen <- render-sandbox-side screen, env, render
163             jump +finish-event
164           }
165         }
166       }
167       +finish-event
168       screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
169       show-screen screen
170     }
171     loop
172   }
173 ]
174 
175 def resize screen:&:screen, env:&:environment -> env:&:environment, screen:&:screen [
176   local-scope
177   load-ingredients
178   clear-screen screen  # update screen dimensions
179   width:num <- screen-width screen
180   divider:num, _ <- divide-with-remainder width, 2
181   # update recipe editor
182   recipes:&:editor <- get *env, recipes:offset
183   right:num <- subtract divider, 1
184   *recipes <- put *recipes, right:offset, right
185   # reset cursor (later we'll try to preserve its position)
186   *recipes <- put *recipes, cursor-row:offset, 1
187   *recipes <- put *recipes, cursor-column:offset, 0
188   # update sandbox editor
189   current-sandbox:&:editor <- get *env, current-sandbox:offset
190   left:num <- add divider, 1
191   *current-sandbox <- put *current-sandbox, left:offset, left
192   right:num <- subtract width, 1
193   *current-sandbox <- put *current-sandbox, right:offset, right
194   # reset cursor (later we'll try to preserve its position)
195   *current-sandbox <- put *current-sandbox, cursor-row:offset, 1
196   *current-sandbox <- put *current-sandbox, cursor-column:offset, left
197 ]
198 
199 # Variant of 'render' that updates cursor-row and cursor-column based on
200 # before-cursor (rather than the other way around). If before-cursor moves
201 # off-screen, it resets cursor-row and cursor-column.
202 def render-without-moving-cursor screen:&:screen, editor:&:editor -> last-row:num, last-column:num, screen:&:screen, editor:&:editor [
203   local-scope
204   load-ingredients
205   return-unless editor, 1/top, 0/left
206   left:num <- get *editor, left:offset
207   screen-height:num <- screen-height screen
208   right:num <- get *editor, right:offset
209   curr:&:duplex-list:char <- get *editor, top-of-screen:offset
210   prev:&:duplex-list:char <- copy curr  # just in case curr becomes null and we can't compute prev
211   curr <- next curr
212   +render-loop-initialization
213   color:num <- copy 7/white
214   row:num <- copy 1/top
215   column:num <- copy left
216   # save before-cursor
217   old-before-cursor:&:duplex-list:char <- get *editor, before-cursor:offset
218   # initialze cursor-row/cursor-column/before-cursor to the top of the screen
219   # by default
220   *editor <- put *editor, cursor-row:offset, row
221   *editor <- put *editor, cursor-column:offset, column
222   top-of-screen:&:duplex-list:char <- get *editor, top-of-screen:offset
223   *editor <- put *editor, before-cursor:offset, top-of-screen
224   screen <- move-cursor screen, row, column
225   {
226     +next-character
227     break-unless curr
228     off-screen?:bool <- greater-or-equal row, screen-height
229     break-if off-screen?
230     # if we find old-before-cursor still on the new resized screen, update
231     # editor.cursor-row and editor.cursor-column based on
232     # old-before-cursor
233     {
234       at-cursor?:bool <- equal old-before-cursor, prev
235       break-unless at-cursor?
236       *editor <- put *editor, cursor-row:offset, row
237       *editor <- put *editor, cursor-column:offset, column
238       *editor <- put *editor, before-cursor:offset, old-before-cursor
239     }
240     c:char <- get *curr, value:offset
241     <character-c-received>
242     {
243       # newline? move to left rather than 0
244       newline?:bool <- equal c, 10/newline
245       break-unless newline?
246       # clear rest of line in this window
247       clear-line-until screen, right
248       # skip to next line
249       row <- add row, 1
250       column <- copy left
251       screen <- move-cursor screen, row, column
252       curr <- next curr
253       prev <- next prev
254       loop +next-character
255     }
256     {
257       # at right? wrap. even if there's only one more letter left; we need
258       # room for clicking on the cursor after it.
259       at-right?:bool <- equal column, right
260       break-unless at-right?
261       # print wrap icon
262       wrap-icon:char <- copy 8617/loop-back-to-left
263       print screen, wrap-icon, 245/grey
264       column <- copy left
265       row <- add row, 1
266       screen <- move-cursor screen, row, column
267       # don't increment curr
268       loop +next-character
269     }
270     print screen, c, color
271     curr <- next curr
272     prev <- next prev
273     column <- add column, 1
274     loop
275   }
276   # save first character off-screen
277   *editor <- put *editor, bottom-of-screen:offset, curr
278   *editor <- put *editor, bottom:offset, row
279   return row, column
280 ]
281 
282 scenario point-at-multiple-editors [
283   local-scope
284   trace-until 100/app  # trace too long
285   assume-screen 30/width, 5/height
286   # initialize both halves of screen
287   assume-resources [
288     [lesson/recipes.mu] <- [
289       |abc|
290     ]
291   ]
292   env:&:environment <- new-programming-environment resources, screen, [def]  # contents of sandbox editor
293   # focus on both sides
294   assume-console [
295     left-click 1, 1
296     left-click 1, 17
297   ]
298   # check cursor column in each
299   run [
300     event-loop screen, console, env, resources
301     recipes:&:editor <- get *env, recipes:offset
302     5:num/raw <- get *recipes, cursor-column:offset
303     sandbox:&:editor <- get *env, current-sandbox:offset
304     7:num/raw <- get *sandbox, cursor-column:offset
305   ]
306   memory-should-contain [
307     5 <- 1
308     7 <- 17
309   ]
310 ]
311 
312 scenario edit-multiple-editors [
313   local-scope
314   trace-until 100/app  # trace too long
315   assume-screen 30/width, 5/height
316   # initialize both halves of screen
317   assume-resources [
318     [lesson/recipes.mu] <- [
319       |abc|
320     ]
321   ]
322   env:&:environment <- new-programming-environment resources, screen, [def]  # contents of sandbox
323   render-all screen, env, render
324   # type one letter in each of them
325   assume-console [
326     left-click 1, 1
327     type [0]
328     left-click 1, 17
329     type [1]
330   ]
331   run [
332     event-loop screen, console, env, resources
333     recipes:&:editor <- get *env, recipes:offset
334     5:num/raw <- get *recipes, cursor-column:offset
335     sandbox:&:editor <- get *env, current-sandbox:offset
336     7:num/raw <- get *sandbox, cursor-column:offset
337   ]
338   screen-should-contain [
339     .           run (F4)           .  # this line has a different background, but we don't test that yet
340     .a0bc           ╎d1ef          .
341     .               ╎──────────────.
342     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
343     .               ╎              .
344   ]
345   memory-should-contain [
346     5 <- 2  # cursor column of recipe editor
347     7 <- 18  # cursor column of sandbox editor
348   ]
349   # show the cursor at the right window
350   run [
351     cursor:char <- copy 9251/␣
352     print screen, cursor
353   ]
354   screen-should-contain [
355     .           run (F4)           .
356     .a0bc           ╎d1␣f          .
357     .               ╎──────────────.
358     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
359     .               ╎              .
360   ]
361 ]
362 
363 scenario editor-in-focus-keeps-cursor [
364   local-scope
365   trace-until 100/app  # trace too long
366   assume-screen 30/width, 5/height
367   assume-resources [
368     [lesson/recipes.mu] <- [
369       |abc|
370     ]
371   ]
372   env:&:environment <- new-programming-environment resources, screen, [def]
373   render-all screen, env, render
374   # initialize programming environment and highlight cursor
375   assume-console []
376   run [
377     event-loop screen, console, env, resources
378     cursor:char <- copy 9251/␣
379     print screen, cursor
380   ]
381   # is cursor at the right place?
382   screen-should-contain [
383     .           run (F4)           .
384     .␣bc            ╎def           .
385     .               ╎──────────────.
386     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
387     .               ╎              .
388   ]
389   # now try typing a letter
390   assume-console [
391     type [z]
392   ]
393   run [
394     event-loop screen, console, env, resources
395     cursor:char <- copy 9251/␣
396     print screen, cursor
397   ]
398   # cursor should still be right
399   screen-should-contain [
400     .           run (F4)           .
401     .z␣bc           ╎def           .
402     .               ╎──────────────.
403     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎              .
404     .               ╎              .
405   ]
406 ]
407 
408 scenario backspace-in-sandbox-editor-joins-lines [
409   local-scope
410   trace-until 100/app  # trace too long
411   assume-screen 30/width, 5/height
412   assume-resources [
413   ]
414   # initialize sandbox side with two lines
415   test-sandbox-editor-contents:text <- new [abc
416 def]
417   env:&:environment <- new-programming-environment resources, screen, test-sandbox-editor-contents
418   render-all screen, env, render
419   screen-should-contain [
420     .           run (F4)           .
421     .               ╎abc           .
422     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎def           .
423     .               ╎──────────────.
424     .               ╎              .
425   ]
426   # position cursor at start of second line and hit backspace
427   assume-console [
428     left-click 2, 16
429     press backspace
430   ]
431   run [
432     event-loop screen, console, env, resources
433     cursor:char <- copy 9251/␣
434     print screen, cursor
435   ]
436   # cursor moves to end of old line
437   screen-should-contain [
438     .           run (F4)           .
439     .               ╎abc␣ef        .
440     .╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╎──────────────.
441     .               ╎              .
442   ]
443 ]
444 
445 def render-all screen:&:screen, env:&:environment, {render-editor: (recipe (address screen) (address editor) -> number number (address screen) (address editor))} -> screen:&:screen, env:&:environment [
446   local-scope
447   load-ingredients
448   trace 10, [app], [render all]
449   hide-screen screen
450   # top menu
451   trace 11, [app], [render top menu]
452   width:num <- screen-width screen
453   draw-horizontal screen, 0, 0/left, width, 32/space, 0/black, 238/grey
454   button-start:num <- subtract width, 20
455   button-on-screen?:bool <- greater-or-equal button-start, 0
456   assert button-on-screen?, [screen too narrow for menu]
457   screen <- move-cursor screen, 0/row, button-start
458   print screen, [ run (F4) ], 255/white, 161/reddish
459   # dotted line down the middle
460   trace 11, [app], [render divider]
461   divider:num, _ <- divide-with-remainder width, 2
462   height:num <- screen-height screen
463   draw-vertical screen, divider, 1/top, height, 9482/vertical-dotted
464   #
465   screen <- render-recipes screen, env, render-editor
466   screen <- render-sandbox-side screen, env, render-editor
467   <render-components-end>
468   #
469   recipes:&:editor <- get *env, recipes:offset
470   current-sandbox:&:editor <- get *env, current-sandbox:offset
471   sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
472   screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
473   #
474   show-screen screen
475 ]
476 
477 def render-recipes screen:&:screen, env:&:environment, {render-editor: (recipe (address screen) (address editor) -> number number (address screen) (address editor))} -> screen:&:screen, env:&:environment [
478   local-scope
479   load-ingredients
480   trace 11, [app], [render recipes]
481   recipes:&:editor <- get *env, recipes:offset
482   # render recipes
483   left:num <- get *recipes, left:offset
484   right:num <- get *recipes, right:offset
485   row:num, column:num, screen <- call render-editor, screen, recipes
486   clear-line-until screen, right
487   row <- add row, 1
488   <render-recipe-components-end>
489   # draw dotted line after recipes
490   draw-horizontal screen, row, left, right, 9480/horizontal-dotted
491   row <- add row, 1
492   clear-screen-from screen, row, left, left, right
493 ]
494 
495 # replaced in a later layer
496 def render-sandbox-side screen:&:screen, env:&:environment, {render-editor: (recipe (address screen) (address editor) -> number number (address screen) (address editor))} -> screen:&:screen, env:&:environment [
497   local-scope
498   load-ingredients
499   current-sandbox:&:editor <- get *env, current-sandbox:offset
500   left:num <- get *current-sandbox, left:offset
501   right:num <- get *current-sandbox, right:offset
502   row:num, column:num, screen, current-sandbox <- call render-editor, screen, current-sandbox
503   clear-line-until screen, right
504   row <- add row, 1
505   # draw solid line after code (you'll see why in later layers)
506   draw-horizontal screen, row, left, right
507   row <- add row, 1
508   clear-screen-from screen, row, left, left, right
509 ]
510 
511 def update-cursor screen:&:screen, recipes:&:editor, current-sandbox:&:editor, sandbox-in-focus?:bool, env:&:environment -> screen:&:screen [
512   local-scope
513   load-ingredients
514   <update-cursor-special-cases>
515   {
516     break-if sandbox-in-focus?
517     cursor-row:num <- get *recipes, cursor-row:offset
518     cursor-column:num <- get *recipes, cursor-column:offset
519   }
520   {
521     break-unless sandbox-in-focus?
522     cursor-row:num <- get *current-sandbox, cursor-row:offset
523     cursor-column:num <- get *current-sandbox, cursor-column:offset
524   }
525   screen <- move-cursor screen, cursor-row, cursor-column
526 ]
527 
528 # like 'render' for texts, but with colorization for comments like in the editor
529 def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [
530   local-scope
531   load-ingredients
532   return-unless s
533   color:num <- copy 7/white
534   column:num <- copy left
535   screen <- move-cursor screen, row, column
536   screen-height:num <- screen-height screen
537   i:num <- copy 0
538   len:num <- length *s
539   {
540     +next-character
541     done?:bool <- greater-or-equal i, len
542     break-if done?
543     done? <- greater-or-equal row, screen-height
544     break-if done?
545     c:char <- index *s, i
546     <character-c-received>  # only line different from render
547     {
548       # at right? wrap.
549       at-right?:bool <- equal column, right
550       break-unless at-right?
551       # print wrap icon
552       wrap-icon:char <- copy 8617/loop-back-to-left
553       print screen, wrap-icon, 245/grey
554       column <- copy left
555       row <- add row, 1
556       screen <- move-cursor screen, row, column
557       loop +next-character  # retry i
558     }
559     i <- add i, 1
560     {
561       # newline? move to left rather than 0
562       newline?:bool <- equal c, 10/newline
563       break-unless newline?
564       # clear rest of line in this window
565       {
566         done?:bool <- greater-than column, right
567         break-if done?
568         space:char <- copy 32/space
569         print screen, space
570         column <- add column, 1
571         loop
572       }
573       row <- add row, 1
574       column <- copy left
575       screen <- move-cursor screen, row, column
576       loop +next-character
577     }
578     print screen, c, color
579     column <- add column, 1
580     loop
581   }
582   was-at-left?:bool <- equal column, left
583   clear-line-until screen, right
584   {
585     break-if was-at-left?
586     row <- add row, 1
587   }
588   move-cursor screen, row, left
589 ]
590 
591 # ctrl-l - redraw screen (just in case it printed junk somehow)
592 
593 after <global-type> [
594   {
595     redraw-screen?:bool <- equal c, 12/ctrl-l
596     break-unless redraw-screen?
597     screen <- render-all screen, env:&:environment, render
598     sync-screen screen
599     loop +next-event
600   }
601 ]
602 
603 # ctrl-n - switch focus
604 # todo: test this
605 
606 after <global-type> [
607   {
608     switch-side?:bool <- equal c, 14/ctrl-n
609     break-unless switch-side?
610     sandbox-in-focus?:bool <- get *env, sandbox-in-focus?:offset
611     sandbox-in-focus? <- not sandbox-in-focus?
612     *env <- put *env, sandbox-in-focus?:offset, sandbox-in-focus?
613     screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env
614     loop +next-event
615   }
616 ]
617 
618 ## helpers
619 
620 def draw-vertical screen:&:screen, col:num, y:num, bottom:num -> screen:&:screen [
621   local-scope
622   load-ingredients
623   style:char, style-found?:bool <- next-ingredient
624   {
625     break-if style-found?
626     style <- copy 9474/vertical
627   }
628   color:num, color-found?:bool <- next-ingredient
629   {
630     # default color to white
631     break-if color-found?
632     color <- copy 245/grey
633   }
634   {
635     continue?:bool <- lesser-than y, bottom
636     break-unless continue?
637     screen <- move-cursor screen, y, col
638     print screen, style, color
639     y <- add y, 1
640     loop
641   }
642 ]