Text Editor v2 contd (October 19, 2020)

Jump to TL;DR (summary)
Jump to Table Of Contents

Starting where we left off last week

Below is the code we have written for the text editor last week:

; A TextEditor is a (make-te String Nat)
(define-struct te [text cursor])
; and represents a text editor
; - where text is the text you have typed
; - and cursor is the index of the cursor

(define TE-EMPTY (make-te "" 0))
(define TE1 (make-te "hello" 1))
(define TE2 (make-te "ECS" 0))

; insert-text : TextEditor KeyEvent -> TextEditor
; Insert text the user typed at the cursor's location
(check-expect (insert-text TE1 "a")l
              (make-te "haello" 2))
(check-expect (insert-text TE2 "right")
              (make-te "ECS" 1))

We had created a simple structure for the text editor which can hold both the text and the cursor position.

We have accidentally skipped a step between the TextEditor structure and the insert-text function. We need to make a template.

TextEditor template

We are now going to design the template. See ECS Help Sheet's Designing Templates section.

(define (te-template te)
    (... (te-text te)
         (te-cursor te)
         ...))

Since both of these are primitives or things that we have not defined ourselves, we do not have to call or make any more templates, we can just use them directly.

Figuring out what stays the same

See: ECS Help Sheet's Designing Programs section for more information on designing a program

Lets make some constants. We know that the background will stay the same, so lets make that

(define BACKGROUND (rectangle 600 200 "solid" "light blue"))

We also know that the font color, font size, and an image for the cursor will stay the same:

(define FONT-COLOR "black")
(define FONT-SIZE 20)
(define CURSOR-IMAGE (rectangle 2 50 "solid" "black"))

Remember, we can always come back and define more constants, these are just the ones we can think of right now. And remember, putting things in constants will help a lot since you only have to change them in one spot, and not all over your program if you want to change their value

Figuring out what clauses we need

Now that we have some constants, we can move to step 2. We need to figure out what clauses we need. We will probably need to-draw so that we can draw the world, we want on-mouse so that we can click and move the cursor around, and lastly we want on-key so we can capture the user's typing and display it.

Our main function

Finally, we are here!!1! We can design our main function. Lets call this microsoft-word (this might violate tons of trademark law, but we are too cool for trademark law). We also need to figure out what goes in and what comes out. With most all main functions, they will take in the WorldState and return a WorldState. And now, we can put it all together with a nice description as well

; microsoft-word : TextEditor -> TextEditor
; Starts up a text editor where you can move the cursor
(define (microsoft-word initial-editor)
    (big-bang initial-editor
              [to-draw draw-text-editor]
              [on-mouse curse-of-the-cursor] ; Remember, these can be named whatever we want, as
                                             ; long as we know what they mean
              [on-key insert-text]))

Now that we have the main function and a wishlist of all the functions we want, we can make their function signatures so we know what we want later when we design them.

Function signatures

Lets define our draw-text-editor signature. Since it is the to-draw handler, it takes in the WorldState and outputs the drawn Image. We can put it in a nice old comment with a description

; draw-text-editor : TextEditor -> Image
; Draws the text with the cursor at a certain position

Now that we have draw-text-editor, we can define the curse-of-the-cursor. This function will be in the clause on-mouse. Because of that, it gives us some special things. As always, it will give us the WorldState and it will also give us the x and y coordinate of the mouse and a MouseEvent defining what the mouse did, and it returns a new modified (or not) WorldState. We can plop that into a handy comment for us.

; curse-of-the-cursor : TextEditor Number Number MouseEvent -> TextEditor
; Change the position of the cursor when you click

And we had already created the insert-text function last week:

; insert-text : TextEditor KeyEvent -> TextEditor
; Insert text the user typed at the cursor's location

Defining functions

Now that we have all of the signatures in our wish list, we can start writing the functions.

We can start with the draw-text-editor function. As always, we need to start with some check expects.

(check-expect
    (draw-text-editor TE-EMPTY)
    (overlay (beside CURSOR_IMAGE (text " " FONT-SIZE FONT-COLOR)) ; This will create the blank
                                                                   ; image with just a cursor
    BACKGROUND))
(check-expect
    (draw-text-editor TE1)
    (overlay 
        (beside (text "h"  FONT-SIZE FONT-COLOR)
                CURSOR_IMAGE
                (text "ello" FONT-SIZE FONT-COLOR)) ; Here we put the cursor between the `h` and
                                                    ; `ello` since it is a cursor pos 1
    BACKGROUND))

Now we can go and define the draw-text-editor function body. So, we want to have 2 states, either there is no text in the editor, or there is some text. we can check for such using a cond.

(define (draw-text-editor editor)
    (cond [...]
          [...]))

Now we need to somehow get the text out of the editor structure. We can do so by using our template. Lets paste it down here so we can use it.

(define (te-template te)
    (... (te-text te)
         (te-cursor te)
         ...))

To access the text, we can use the te-text helper function from our template. So we can paste it into the first cond. We need to check it to make sure that its length is > 0, meaning it is not empty

(define (draw-text-editor editor)
    (cond [(> (string-length (te-text editor)) 0)] ; new
          [...]))

Now we can define a simple template of what we will do. Remember, we want to write some text, the cursor, and then the rest of the text

(define (draw-text-editor editor)
    (cond [(> (string-length (te-text editor)) 0)
           (overlay (beside (text ... FONT-SIZE FONT-COLOR)    ; new
                            CURSOR-IMAGE                       ; new
                            (text ... FONT-SIZE FONT-COLOR))   ; new
                    BACKGROUND)]                               ; new
          [...]))

Now, we need to figure out how to draw the text in front of the cursor. We can get a slice of a string by using the substring function, and use the cursor position to chop it. We want to get the substring of the te-text from 0 up until te-cursor

(define (draw-text-editor editor)
    (cond [(> (string-length (te-text editor)) 0)
           (overlay (beside (text (substring (te-text editor) 0 ; new
                                             (te-cursor))       ; new
                                   FONT-SIZE FONT-COLOR)
                            CURSOR-IMAGE
                            (text ...))
                    BACKGROUND)]
          [...]))

And now that we have the first text, we can do the second text very much the same, except this time, we don't need to give substring an ending point, because in doing so, substring will just go until the end.

(define (draw-text-editor editor)
    (cond [(> (string-length (te-text editor)) 0)
           (overlay (beside (text (substring (te-text editor) 0
                                             (te-cursor))
                                   FONT-SIZE FONT-COLOR)
                            CURSOR-IMAGE
                            (text (substring (te-text editor)   ; new
                                             (te-cursor))       ; new
                                  FONT-SIZE FONT-COLOR))
                    BACKGROUND)]
          [...]))

Now that we have the text having state, we can work on the section of the function when there is no text to draw. Since we will draw the same thing every time you have no text, we can extract it into a constant. We can copy it from our first check expect

(define BLANK-EDITOR-IMAGE
        (overlay (beside CURSOR_IMAGE (text " " FONT-SIZE FONT-COLOR)) ; This will create the blank
                                                                       ; image with just a cursor
        BACKGROUND))

and then update our check expect from

(check-expect
    (draw-text-editor TE-EMPTY)
    (overlay (beside CURSOR_IMAGE (text " " FONT-SIZE FONT-COLOR)) ; This will create the blank
                                                                   ; image with just a cursor
    BACKGROUND))

to

(check-expect
    (draw-text-editor TE-EMPTY)
    BLANK-EDITOR-IMAGE) ; changed

and we can add that into our new function

(define (draw-text-editor editor)
    (cond [(> (string-length (te-text editor)) 0)
           (overlay (beside (text (substring (te-text editor) 0
                                             (te-cursor))
                                   FONT-SIZE FONT-COLOR)
                            CURSOR-IMAGE
                            (text (substring (te-text editor)
                                             (te-cursor))
                                  FONT-SIZE FONT-COLOR))
                    BACKGROUND)]
          [else BLANK-EDITOR-IMAGE])) ; new

Too Long; Didn't Read

  • USE YOUR ECS Help Sheet. It will walk you through it all.
  • Make constants! they will help you keep duplicated code to a minimum, and will save you when you need to change them.
  • Complex programs can be broken down into simpler parts and are a lot easier when you do so
  • WRITE COMMENTS. Since we were unable to finish the text editor today, we will thank ourselves later when we have to come back to it, that we have left comments telling us about what we still need to do. (Your memory can only serve you so well)