eno writer

002 - a big blank page / death by a thousand features / text wrapping

eno writer

Each week, I reflect on my experience working on eno, the modern word processor I am writing from scratch. Remarks are ordered from general to technical. If you enjoy the post, please subscribe at the bottom to get the next one.

A very big blank page

I had forgotten the psychological element of starting a new project from zero. It's hard to stare at the enormous blank page that needs to be filled and not feel completely helpless. Often, I need to force myself to just start writing code and see what happens. It can take an hour of spinning wheels but every time, I eventually find myself drawn into work and end up frustrated when I have to stop work for the day. Sometimes this means I can pick up instantly the next morning, but other times it takes a lot of discipline to get moving again.

It helps to work on things that have some visual feedback because you get a nice dopamine hit when you get them working. Of course this has to be balanced with going back to lay good foundations. In the initial weeks, I had a streak where every day I added at least one small feature to the program. It felt as though As the features started to have more complex requirements, it became increasingly difficult to add the next one. I had to give up on my streak and lay some strong foundations to build upon.

Death by a thousand features

In response to my announcement of eno last week, a number of people made comments about how Word has such a ridiculous number of options. This is very true and something I want to fix. A huge proportion of these options relate to formatting. Word lets you create any style you can dream of. It even has the Word Art feature if you want to put 3d text in your document.

The funny thing is that most professionals want all their documents to look the same. I've spoken with many lawyers about their frustration trying to get a document to look exactly the way every other document of that type is supposed to look. In these cases, the countless formatting features of Word are actively working against them!

If someone frequently makes the same mistake, one of the best ways to help them is by making that mistake impossible. For instance, after our two year old put a dent in my kitchen floor with a large pot, we moved all our pots and pans from the bottom drawer to the top of the stove. There have been no new dents from pots/pans since. And so, rest assured, there will be no custom formatting allowed in eno and the lives of many professionals will be the better for it.

Text Wrapping

Text is so ubiquitously displayed on computer screens that we take it for granted as a solved problem. If you were to write an .html file for a new webpage and put a bunch of text in a div, when that text reached the edge of the screen, it would wrap around to the other side and continue. Just like a book!

Computers do not know how to do this inherently. Every time this happens, code is running to calculate where the wrapping should occur. In a web browser, we don't think about this at all. In lower level code, there are libraries available that take care of this for us.

The downside of this though, is that a programmer cannot easily know where particular characters on the screen appear. I can tell the computer to put this paragraph of text on a web page, but I cannot easily find out the exact pixel location where the word paragraph occurs. There are approaches that can be taken to get this information, but it becomes a sort of reverse engineering. There is some process on this computer that knew where the word paragraph was written - if we were in that process we could just keep track of that information rather than figuring it out after the fact.

Undoubtedly, when writing eno, I will want to know where certain words occur, and so I want to do my own text layout. This is made much simpler because I am using a terminal interface where every character takes up the exact same width. Normally, the letter "l" takes much less space than the letter "o", adding a whole other layer of complexity to the wrapping problem.

In any event, here is what I came up with:

fn wrapText(box: *Box, cursor: *Point) void {
    const parent = box.parent.?;
    const parent_width = parent.calc_size[@intFromEnum(Axis2.x)];
    const remaining = parent_width - cursor.x;
    const text = box.text.?;

    if (text.len <= remaining) {
        // box fits as is, just position and return
        <BOX POSITIONING CODE>
        return;
    }

    // seek ahead to the next whitespace character to see if we can split
    const split_at: usize = r: {
        var keep_until: usize = 0;
        for (text[0..], 0..) |c, idx| {
            if (idx < remaining and c == ' ') {
                keep_until = idx + 1;
            }
            if (idx >= remaining and keep_until > 0) {
                break :r keep_until;
            }
            if (idx > parent_width and keep_until == 0) {
                // there is no whitespace in the entire width of the parent
                // so we just need to split at an arbitrary location
                break :r remaining;
            }
        } else {
            // we will be able to break but need to wrap down a line first
            assert(keep_until == 0);
            break :r 0;
        }
    };

    if (split_at == 0) {
        // we cannot fit any more words onto the current line so we need
        // to wrap right away
        cursor.x = 0;
        cursor.y += 1;
        wrapText(box, cursor);
        return;
    }

    // fill remainder of this line with current box and wrap subsequent
    // text to new_box
    const new_box = <NEW BOX INIT CODE>
    new_box.text = text[split_at..];
    box.text = text[0..split_at];
    box.wraps_to = new_box;
    insertBox(parent, box, new_box);    
    <BOX POSITIONING CODE>
    cursor.x = 0;
    cursor.y += 1;
    wrapText(new_box, cursor);
}

Earlier in the program, we start by naively writing all the text into a single box that is 1 cell high and as many cells long as the length of the text. You can picture this as a single line of text that just runs off the side of the paragraph and even off the right of your screen. We then pass that into wrapText which will take that line and break into multiple lines that are positioned vertically down the screen.

Inside wrapText I have a cursor which tracks where we are in the box we are drawing into. We also know the width of this box, so width - cursor.x represents the remaining available space we can draw into on the current line. We then look through the text to see how many complete words we can fit in the remaining space. If we can fit some of the words, we split the box into two parts. The first part contains the words that fit on the current line. The second box gets moved down a line and we perform wrapText() again on that. This creates a recursive chain where we just keep wrapping the line over and over again until there is nothing left to wrap.

It took me a while to hone in on this solution and it's one of the first real world programming challenges I've worked with where recursion was so clearly the tool for the job. I am also very indebted to Ryan Fleury's articles on UI and help he offered through his Hidden Grove community. I am pretty happy with it today, though I am sure that will change in a week's time. And yes, I know it doesn't support unicode. I'm choosing to ignore that for the time being but I think it will just amount to tracking more data.


Subscribe below to get notifications for new posts.

Powered by Buttondown.

We also have and RSS feed