Template repo for tiny cross-platform apps that can be modified on phone, tablet or computer.

Building blocks for your Freewheeling Apps#

Freewheeling apps consist of 3 kinds of things:

  • a small number of functions you can define that get automatically called for you as appropriate,
  • a wide variety of primitives that you can call but not modify,
  • any other function you can define and call at will.

The rest of this document will summarize what is available to you in the first two categories.

Variables you can set/read#

  • Disallow_error_recovery_on_key_release -- set this when you detect hard-to-recover-from errors. Right now I'm only aware of one such (see driver.love): calls to love.graphics.newCanvas can fail at the SDL2 layer if a computer doesn't have enough video RAM to perform them. In response, set this variable before and unset after to prevent the system from repeatedly retrying something that is unlikely to recover. In particular, prevent settings from being saved to disk during an error state, thereby perpetuating the error.

    This variable is subject to removal if I can come up with a better approach, but I'll document it here as long as I support it. Please let me know if you find a new need to read or write it.

Functions you can implement that will get automatically called#

  • on.initialize(arg) -- called when app starts up. Provides in arg an array of words typed in if you ran it from a terminal window. (Based on LÖVE.)

  • on.draw() -- called to draw on the window, around 30 times a second. (Based on LÖVE.)

  • on.update(dt) -- called after every call to on.draw. Make changes to your app's variables here rather than in on.draw. Provides in dt the time since the previous call to on.update, which can be useful for things like smooth animations. (Based on LÖVE.)

  • on.mouse_press(x,y, mouse_button, is_touch, presses) -- called when you press down on a mouse button. Provides in x and y the point on the screen at which the click occurred, and in mouse_button an integer id of the mouse button pressed. 1 is the primary mouse button (the left button on a right-handed mouse), 2 is the secondary button (the right button on a right-handed mouse), and 3 is the middle button. Further buttons are mouse-dependent. (Based on LÖVE.)

  • on.mouse_release(x,y, mouse_button, is_touch, presses) -- called when you release a mouse button. Provides the same arguments as on.mouse_press() above. (Based on LÖVE.)

  • on.mouse_move(x,y, dx,dy, is_touch) -- called when you move the mouse. Provides in x and y the point on the screen at which the click occurred and in dx and dy the amount moved since the previous call of mouse_move. (Based on LÖVE.)

  • on.mouse_wheel_move(dx,dy) -- called when you use the scroll wheel on a mouse that has it. Provides in dx and dy an indication of how fast the wheel is being scrolled. Positive values for dx indicate movement to the right. Positive values for dy indicate upward movement. (Based on LÖVE.)

  • on.touch_press(touch_id, x,y, dx,dy, pressure) -- called when you touch a multi-touch screen. Provides in x and y the point on the screen at which the touch occurred. (Based on LÖVE.)

  • on.touch_release(touch_id, x,y, dx,dy, pressure) -- called when you release a touch. Provides the same arguments as on.touch_press() above. (Based on LÖVE.)

  • on.touch_move(touch_id, x,y, dx,dy, pressure) -- called when you move any finger on a multi-touch screen. Provides the same arguments as on.touch_press() above, with dx and dy containing the amount moved since the previous call to touch_move. (Based on LÖVE.)

  • on.keychord_press(chord, key, scancode, is_repeat) -- called when you press a key-combination. Provides in key a string name for the key most recently pressed (valid values). Provides in chord a string representation of the current key combination, consisting of the key with the following prefixes:

    • C- if one of the ctrl keys is pressed,
    • M- if one of the alt keys is pressed,
    • S- if one of the shift keys is pressed, and
    • s- if the windows/cmd/super key is pressed.
  • on.text_input(t) -- called when you press a key combination that yields (roughly) a printable character. For example, shift and a pressed together will call on.textinput with A. (Based on LÖVE.)

  • on.key_release(key, scancode) -- called when you press a key on the keyboard. Provides in key a string name for the key (valid values). (Based on LÖVE, including other variants.)

  • on.quit() -- called before the app shuts down. (Based on LÖVE.)

  • on.save_settings() -- called after on.quit and should return a table which will be saved to disk.

  • on.load_settings(settings) -- called when app starts up, before on.initialize. Provides in settings the table that was saved to disk the last time the app shut down.

  • on.focus(start?) -- called when the app starts or stops receiving keypresses. start? will be true when app starts receiving keypresses and false when keypresses move to another window. (Based on LÖVE.)

  • on.resize(w,h) -- called when you resize the app window. Provides new window dimensions in w and h. (Based on LÖVE)

  • on.code_change() -- called when you make changes to the app using driver.love, any time you hit f4 inside driver.love, after a definition is created or modified.

  • on.mouse_focus(in_focus) -- called when the mouse pointer moves on or off the app window. in_focus will be true when mouse comes within the window area, and false when it goes off. (Based on LÖVE.)

Functions you can call#

Everything in the LÖVE and Lua guides is available to you.

text editor primitives#

  • editor = edit.new(top, left, right, bottom, font, font_height) -- returns an object that can be used to render an interactive editor widget for text within the given bounds. Wraps long lines at word boundaries where possible, or in the middle of words (no hyphenation yet) when it must.

  • edit.resize(editor, w, h, right, bottom) -- adjusts screen dimensions and editor bounds.

  • edit.load_file(editor, filename) -- loads the editor state with lines from filename.

Make sure to wire up the following functions from corresponding on.* functions:

  • edit.draw(editor, fg_color, hide_cursor, show_line_numbers, cursor_color)
    • fg_color is either nil or a 3-array of rgb or a 4-array of rgba. If it's nil, the text will be syntax highlighted.
    • hide_cursor is a boolean
    • show_line_numbers is a boolean
    • cursor_color is an optional color
  • edit.keychord_press(editor, chord, key, scancode, is_repeat, readonly)
    • key and scancode are for the final key pressed in the chord
    • readonly should probably be in sync with hide_cursor in edit.draw
  • edit.text_input(editor, t)
  • edit.key_release(editor, key, scancode)
  • edit.mouse_press(editor, x,y, mouse_button, is_touch, presses)
  • edit.mouse_move(editor, x,y, dx,dy, is_touch)
  • edit.mouse_release(editor, x,y, mouse_button, is_touch, presses)
  • edit.mouse_wheel_move(editor, dx,dy)
  • edit.touch_press(editor, touch_id, x,y, dx,dy, pressure)
  • edit.touch_move(editor, touch_id, x,y, dx,dy, pressure)
  • edit.touch_release(editor, touch_id, x,y, dx,dy, pressure)

The touch_* functions above implement inertial scroll. You'll also need to wire up two additional functions for inertial scroll:

  • edit.update_inertial_scroll(editor, dt) -- from on.update
  • edit.maybe_stop_inertial_scroll(editor) -- from on.mouse_press, at a higher priority than edit.touch_press or any other UI elements on screen. Will return true if any inertial scroll was in progress and stopped.

Basic example showing how to wire together these calls into a live-modifiable text editor.

Other optional configuration:

  • edit.update_font_settings(editor, font_height) -- updates all state dependent on font height.

  • edit.save_to_disk(editor) -- save current state to editor.filename

  • edit.is_this_love_version_supported() -- might provide early warning if the version of LÖVE is too old.

  • editor.indent_wrapped_lines = true -- now after wrapping the line continues from the same x coordinate as its starting character, rather than the left margin.

The freewheeling protocol with the driver#

Freewheeling apps currently respond to the following commands from the driver:

  • QUIT -- tells the current app to quit
  • RESTART -- tells the current app to reinitialize after saving any settings
  • MANIFEST -- requests a list of definitions the app knows about. The app returns only definitions that were created using the freewheeling framework and so can be modified and errors recovered from.
  • DEFAULT_MAP -- requests a default map of the code. Everyone is free to create their own "memory palace", but this is usually a good default to start with.
  • GET <name> -- requests the source code for definition <name>.
  • GET* <name> ... -- requests source code for multiple definitions.
  • DELETE <name> -- requests deletion of the definition <name>. Only permitted for definitions created using the freewheeling framework, not lower-level definitions.
  • anything else -- is considered a new definition to be loaded into the app.

Commands may cause an error response, which is sent back to the driver.

In addition, any run-time errors caused as the app executes are also sent back to the driver. These don't need an explicit command from the driver.

Some primitives available for complying with the protocol:

  • live.receive_from_driver() -- looks for a message from the driver, and returns nil if there's nothing.

  • live.send_to_driver(msg) -- sends a message to the driver.

  • live.send_run_time_error_to_driver(msg) -- sends an error to the driver. Automatically invoked by the LÖVE error handler, so you shouldn't need to call this.

  • live.get_cmd_from_buffer(buf) -- helper to extract the first word from a command.

  • live.get_binding(name) -- look up the repo for the source code for a name.