Freewheeling Apps https://akkartik.name/freewheeling-apps en-us 2024-07-22 http://akkartik.name/post/2024-07-22-devlog 22 Jul 2024 12:26:08 PDT http://akkartik.name/post/2024-07-22-devlog The problem: implementing text editor operations as lines might wrap or scroll.

e.g. clicking with the mouse to reposition the cursor, or pressing the down arrow (which might cause a scroll)

The key new solution: an API of primitives that make such operations fairly self-evident to build.

  • to_loc: (x, y) -> loc
    Identify the location at pixel coordinates (x,y) on screen.
    Returns nil if (x,y) is not on screen.
  • to_coord: loc -> x, y
    Identify the top-left coordinate on screen of location loc.
    Returns nil if loc is not on screen.
  • down: loc, dy -> loc
    Find the location at the start of a screen line dy pixels down from loc.
    Returns nil if dy is taller than the screen.
    Returns bottom of file if we hit it.
  • up: loc, dy -> loc Find the location at the start of a screen line dy pixels up from loc.
    Returns nil if dy is taller than the screen.
    Returns top of file if we hit it.
  • hor: loc, x -> loc
    Find the location at x=x0 on the same screen line as loc.

I think they might be applicable to any pixel-based editors that use proportional fonts. They seem independent of the data structure used by the editor. I use an array of lines, and so locations are defined as (line_index, pos) tuples, where pos counts in utf-8 code-points.

There's probably a few bugs but hopefully it'll stabilize quickly. I'd appreciate people trying it out:

https://git.sr.ht/~akkartik/lines2.love

Lessons from this experience:

  • There's a "hard part" to programming beyond the reach of tools or methods. Sometimes a problem needs the right "algebraic" abstraction, designed around an invariant and preserving it across any composition of operations.
  • Not all programs get this hard.
  • It's useful to notice when some part calls out for doing this hard, focused work.

I think I now better understand the "abyss". ]]> 2024-07-11 http://akkartik.name/post/2024-07-11-devlog 11 Jul 2024 19:17:37 PDT http://akkartik.name/post/2024-07-11-devlog The code in the screenshot is a function to convert a mouse click (mx, my) into the location (line_index, pos) of the character at that point on the screen.

The problem is much of this function is boilerplate shared with several other places, such as the code to draw text on screen, compute the height of a wrapped line, etc. The boilerplate makes it difficult to see the business logic unique to this particular function, and so creates pressure to prematurely create an abstraction to "DRY things out". Highlighting the shape of the boilerplate in pink helps the eye to focus to the unique business logic in the protrusions, and so counters the pressure to hide the boilerplate before I've figured out the best way to do so.


┌ local y = State.top
  for line_index,line in array.each(State.lines, State.screen_top.line) do
    if line.mode == 'text' then
      local x = State.left
      local initpos = 1
      if line_index == State.screen_top.line then
        initpos = State.screen_top.pos
      end
      for pos, char in utf8chars(line.data, initpos) do
        local w = State.font:getWidth(char)  -- width of char
        if char:match('%s') then
          if line_wrap_at_word_boundary(State, x, line.data, pos) then ┘
            if my < y+State.line_height then return line_index, pos end
         └  x = State.left
            y = y + State.line_height
          else ┘
            if my < y+State.line_height and mx < x+w then return line_index, pos end
          └ x = x + w
          end
        else
          if x+w > State.right then
            x = State.left
            y = y + State.line_height ┘
            if my < y+State.line_height then return line_index, pos end
        └ else ┘
            if my < y+State.line_height and mx < x+w then return line_index, pos end
        └ end
          x = x+w
        end
      end
      y = y + State.line_height
    elseif line.mode == 'drawing' then ┘
      if my < y+State.drawing_height then return line_index end
    └ y = y + h
    end
  end ┘

(As an aside, this is an example of what I think of as "programmer-configured highlighting". We've gotten used to our editors deciding what to highlight for us, and we just pick the colors. One little tool I use everyday is the ability to highlight specific identifiers which flips this around: I pick a word, and the editor picks a color at random to highlight it with. And the highlight persists across sessions. The color of the State variable in the screenshot was selected in this manner.) ]]> 2024-07-10 http://akkartik.name/post/2024-07-10-devlog 10 Jul 2024 18:07:46 PDT http://akkartik.name/post/2024-07-10-devlog photograph of a page from a notebook showing a strange shape a little bit like an axe or an old-style double-edge safety razor blade. It's straight on the left with rounded corners. On the right, is a bumpy, irregular 'blade' with 5 protrusions of irregular lengths. Four of them form two pairs of long+short protrusions, and a fifth is off to the bottom. The four protrusions are for code related to lines of text, labeled:
- at word boundary, need to wrap
- at word boundary, no need to wrap
- word, need to chop
- word, no need to chop

Until now I've been developing the editor the "usual" way, which for me consists of needing some computation, figuring out the most convenient place to perform the computation, then squirreling away the result somewhere it's available when needed. In an effort to get myself out of the rut of the inevitable problems of indirection and cache invalidation that result, I've been trying to replace all my ad hoc data structures with on-demand computation based on the base state of the program. And what I've been ending up with is umpteen variations of this pictured algorithm, just with different code stuck on to the protrusions.

There may be an abstraction that comes out of all this, but I don't see it yet. And as CA says, a flower isn't made up of identical petals. Each one evolves uniquely as a part of the whole.

Here's the Lua code skeleton corresponding to that drawing. The ellipses correspond to protrusions in the drawing:

for line_index, line in array.each(State.lines, State.screen_top.line) do
  if line.mode == 'text' then
    local initpos = 1
    if line_index == State.screen_top.line then
      -- top screen line
      initpos = State.screen_top.pos
    end
    for pos, char in utf8chars(line.data, initpos) do
      if char:match('%s') then
        if line_wrap_at_word_boundary(State) then
          ...
        else
          ...
        end
      else
        if x+w > State.right then
          ...
        else
          ...
        end
      end
    end
  else  -- drawing
    ...
  end
end
]]>
2024-07-03 http://akkartik.name/post/2024-07-03-devlog 03 Jul 2024 07:10:54 PDT http://akkartik.name/post/2024-07-03-devlog a drawing labeling the top, left, right and bottom coordinates of a rectangle in grey, and the height of a single letter 'h' within the rectangle

…might represent a function for initializing a text editor widget with the following signature:

edit.initialize(top, left, right, bottom, font_size)

And the numbers indicate a specific call to this function:

edit.initialize(15, 15, 115, 215, 20)

Interestingly, these alternative semantics would make for a more pleasing glyph.

edit.initialize(margin-top, margin-left, margin-right, margin-bottom, font-size)

a drawing labeling the top, left, right and bottom _margins_ of a rectangle in grey, and the height of a single letter 'h' within the rectangle ]]> 2024-06-30 http://akkartik.name/post/2024-06-30-devlog 30 Jun 2024 16:54:32 PDT http://akkartik.name/post/2024-06-30-devlog (Just a mockup to convey the idea. Plan is just to use this notation with pen and paper.)

Is this picture intelligible without any explanation?

]]> 2024-06-09 http://akkartik.name/post/2024-06-09-devlog 09 Jun 2024 14:26:24 PDT http://akkartik.name/post/2024-06-09-devlog Still several things left to investigate.

  • Really small windows probably crash.
  • Really wide and short windows probably crash.
  • Pressing and releasing mouse really quickly within a single frame will probably crash.

I want to delete some data structures and just recompute them all the time. ]]> 2024-06-07 http://akkartik.name/post/2024-06-07-devlog 07 Jun 2024 22:35:05 PDT http://akkartik.name/post/2024-06-07-devlog

  • t = n: my program was responsible for x and y
  • t = nn: my program became responsible for x, y and z
  • t = nnnnnn: bug in z (interacting with x/y)

Adding z required rethinking the entire program. I tried to patch it in. Implications were not fully worked out. ]]> 2024-06-05 http://akkartik.name/post/2024-06-05-devlog 05 Jun 2024 21:47:53 PDT http://akkartik.name/post/2024-06-05-devlog I wonder if this is the major reason to huddle together on top of jenga stacks with tons of dependencies, terrified of fragmentation: You always need more testing than you think, and there's no way to compete with something that's been through that much testing.

I come stare at this abyss every year or two.

Doesn't do me any good, though.

(This post spawned a sprawling, constructive conversation.) ]]> 2024-05-04 http://akkartik.name/post/2024-05-04-devlog 04 May 2024 06:57:45 PDT http://akkartik.name/post/2024-05-04-devlog

The following program lets you scrub the mouse downward to find more and more precise approximations of π within the red optical sight in the center of the screen.

https://akkartik.itch.io/carousel/devlog/725703/- ]]> 2024-04-24 http://akkartik.name/post/2024-04-24-devlog 24 Apr 2024 15:33:45 PDT http://akkartik.name/post/2024-04-24-devlog To recap, you basically have a line of cells that can be in one of two states ('alive' or 'dead') and rules that decide how a cell's state evolves based on the state of its immediate neighbors to the left and right. The images below show a snapshot of time in a row of pixels, and time advancing from the top row of pixels to the bottom.

Starting from a single live cell, of the 256 rules 16 immediately wink out (empty grids in the picture below), 16 don't change (vertical lines), 48 move the cell (24 each to the left and right), 30 grow into triangles over time (6 each to the left and right and 18 on both), 18 form Sierpinski patterns and 22 are more chaotic. Here's a detail in Lua Carousel where you can see many of these types.

Detail of Lua Carousel browsing the space of possible rules for 1-D cellular automata, with each rule starting from a single live cell and rows further down showing its evolution over time.

However, things look different if you start from a random configuration of live and dead cells. Seemingly well-behaved rules hide subtleties, and seeming patterns vanish.

The same rules as above, but now we're starting from the same random configuration in each rule.

For a given rule, different random initial configurations largely look the same from a distance, which suggests random selection yields more realistic pictures for a rule.

Eye-balling the surface, I think 47/256 rules are chaotic.

Rule 30 is the famous one, but my favorites are rule 150 and 165.

Detail of Lua Carousel focusing on the Rule 150 1-D cellular automaton.

(I've also been skimming Stephen Wolfram's "A New Kind of Science" as I do this. Wolfram separates "nested" from "random" patterns, but that seems to be an artifact of starting with a single live cell. "Nested" patterns (like Sierpinski triangles) are just a milder kind of chaos our visual cortex can get a grip on.)

Starting with a single live cell is 'simple' but grossly incomplete, exercising only scenarios 0, 1, 2, and 4 in the first step.

A couple of generations under high-magnification, showing that a single live cell exercises only 4 rules (each highlighted in a different color).

And if we truly care about simple, why not just start with all dead cells? Rules don't care.

Anyways, I see two fairly simple initial states that exercise every possible scenario in time step 1: the mirror images 10111 and 11101 when surrounded by runs of dead cells.

Now I see only the one real stable rule: rule 204. 204 is binary 11001100, each bit of which is exactly the middle bit of numbers 7-0. In other words, every scenario maps to the central square.

Detail of Lua Carousel showing rule 204. Clear vertical lines down the center show that each generation is identical to the last. You can also see rule 236 poking out near the bottom. It too stabilizes to identical generations, but if you squint the first generation isn't identical.

Even here, though, the long runs of dead cells keep the "true nature" of a rule from coming out. I think random initial conditions do that much better.

Perhaps what would be best is to keep our simple pattern in the center exercising all scenarios, and then pad it with a random initial state. We do have to remember to pad it out with 3 dead cells. Let's do that on both sides for symmetry.

Ah, here's a nice screenshot of a central portion of the ruleset, ideal density for chaos to emerge.

A detail of Lua Carousel running a browser of the 1-D cellular automata. We're zoomed out enough to see 20 rules at once, with others partially visible on the fringes. Fully visible is the rectangular subset centered on rule 151. We're laying out the rules in a 16x16 grid, so the visible rules are 133-137, 149-153, 165-169, 181-185. Even at this zoomed-out scale, rules 135, 137, 149, 150, 151, 153, 165, 169, 182 and 183 are visibly chaotic.

Links

]]>