22 KiB
Building blocks for your Freewheeling Apps
Freewheeling apps consist of 4 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,
- tests that start with
test_
and run on startup or after any change you make to the app's code, and - 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 read
-
Current_time
-- seconds since some unspecified time. Useful for managing durations of time (scheduling operations, etc.). -
App.screen
width
andheight
-- integer dimensions for the app window in pixels.flags
-- some properties of the app window. Seeflags
inlove.graphics.getMode
for details.
-
Version
-- the running version of LÖVE as a string, e.g. '11.4'. -
Major_version
-- just the part before the period as an int, e.g. 11.
Functions you can implement that will get automatically called
-
on.initialize(arg)
-- called when app starts up. Provides inarg
an array of words typed in if you ran it from a terminal window. (Based on LÖVE.) -
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, beforeon.initialize
. Provides insettings
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 betrue
when app starts receiving keypresses andfalse
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 inw
andh
. Don't bother updatingApp.screen.width
andApp.screen.height
, that will happen automatically before callingon.resize
. (Based on LÖVE) -
on.file_drop(file)
-- called when a file icon is dragged and dropped on the app window. Provides infile
an object representing the file that was dropped, that will respond to the following messages:file:getFilename()
returning a string namefile:read()
returning the entire file contents in a single string
(Based on LÖVE.)
-
on.code_change()
-- called when you make changes to the app using driver.love, any time you hitf4
inside driver.love, after a definition is created or modified. -
on.draw()
-- called to draw on the window, around 30 times a second. (Based on LÖVE.) -
on.update(dt)
-- called after every call toon.draw
. Make changes to your app's variables here rather than inon.draw
. Provides indt
the time since the previous call toon.update
, which can be useful for things like smooth animations. (Based on LÖVE.) -
on.mouse_press(x,y, mouse_button)
-- called when you press down on a mouse button. Provides inx
andy
the point on the screen at which the click occurred, and inmouse_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), and3
is the middle button. Further buttons are mouse-dependent. (Based on LÖVE.) -
on.mouse_release(x,y, mouse_button)
-- called when you release a mouse button. Provides the same arguments ason.mouse_press()
above. (Based on LÖVE.) -
on.mouse_move(x,y, dx,dy, istouch)
-- called when you move the mouse. Provides inx
andy
the point on the screen at which the click occurred and indx
anddy
the amount moved since the previous call ofmouse_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 indx
anddy
an indication of how fast the wheel is being scrolled. Positive values fordx
indicate movement to the right. Positive values fordy
indicate upward movement. (Based on LÖVE.) -
on.mouse_focus(in_focus)
-- called when the mouse pointer moves on or off the app window.in_focus
will betrue
when mouse comes within the window area, andfalse
when it goes off. (Based on LÖVE.) -
on.keychord_press(chord, key)
-- called when you press a key-combination. Provides inkey
a string name for the key most recently pressed (valid values). Provides inchord
a string representation of the current key combination, consisting of the key with the following prefixes:C-
if one of thectrl
keys is pressed,M-
if one of thealt
keys is pressed,S-
if one of theshift
keys is pressed, ands-
if thewindows
/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
anda
pressed together will callon.textinput
withA
. (Based on LÖVE.) -
on.key_release(key)
-- called when you press a key on the keyboard. Provides inkey
a string name for the key (valid values). (Based on LÖVE, including other variants.)
Functions you can call
Everything in the LÖVE and
Lua guides is available to you,
but here's a brief summary of the most useful primitives. Some primitives have
new, preferred names under the App
namespace, often because these variants
are more testable. If you run them within a test you'll be able to make
assertions on their side-effects.
regarding the app window
-
width, height, flags = App.screen.size()
-- returns the dimensions and some properties of the app window. (Based on LÖVE.) -
App.screen.resize(width, height, flags)
-- modify the size and properties of the app window. The OS may or may not act on the request. (Based on LÖVE.) -
x, y, displayindex = App.screen.position()
-- returns the coordinates and monitor index (if you have more than one monitor) for the top-left corner of the app window. (Based on LÖVE.) -
App.screen.move(x, y, displayindex)
-- moves the app window so its top-left corner is at the specified coordinates of the specified monitor. The OS may or may not act on the request. (Based on LÖVE.)
drawing to the app window
-
App.screen.print(text, x,y)
-- print the giventext
in the current font using the current color so its top-left corner is at the specified coordinates of the app window. (Based on LÖVE.) -
love.graphics.getFont()
-- returns a representation of the current font. (From LÖVE.) -
love.graphics.setFont(font)
-- switches the current font tofont
. (From LÖVE.) -
love.graphics.newFont(filename)
-- creates a font from the given font file. (From LÖVE, including other variants.) -
App.width(text)
returns the width oftext
in pixels when rendered using the current font. (Based on LÖVE.) -
App.color(color)
-- sets the current color based on the fieldsr
,g
,b
anda
(for opacity) of the tablecolor
. (Based on LÖVE.) -
love.graphics.line(x1,y1, x2,y2)
-- draws a line from (x1
,y1
) to (x2
,y2
) in the app window using the current color, clipping data for negative coordinates and coordinates outside (App.screen.width
,App.screen.height
) (From LÖVE, including other variants.) -
love.graphics.rectangle(mode, x, y, w, h)
-- draws a rectangle using the current color, with a top-left corner at (x
,y
), with dimensionswidth
along the x axis andheight
along the y axis (though check out https://love2d.org/wiki/love.graphics for ways to scale and rotate shapes).mode
is a string, either'line'
(to draw just the outline) and'fill'
. (From LÖVE, including other variants.) -
love.graphics.circle(mode, x, y, r)
-- draws a circle using the current color, centered at (x
,y
) and with radiusr
.mode
is a string, either'line'
and'fill'
. (From LÖVE, including other variants.) -
love.graphics.arc(mode, x, y, r, angle1, angle2)
-- draws an arc of a circle using the current color, centered at (x
,y
) and with radiusr
.mode
is a string, either'line'
and'fill'
.angle1
andangle2
are in radians. (From LÖVE, including other variants.)
There's much more I could include here; check out the LÖVE manual.
text editor primitives
The text-editor widget includes extremely thorough automated tests to give you early warning if you break something.
-
state = edit.initialize_state(top, left, right, font, font_height, line_height)
-- returns an object that can be used to render an interactive editor widget for text starting aty=top
on the app window, betweenx=left
andx=right
. Wraps long lines at word boundaries where possible, or in the middle of words (no hyphenation yet) when it must. -
state.lines = load_array(lines)
-- loads the editor state with the array oflines
-
edit.quit()
-- calling this ensures any final edits are flushed to disk before the app exits. -
edit.draw(state, fg, hide_cursor)
-- call this fromon.draw
to display the current editor state on the app window as requested in the call toedit.initialize_state
that createdstate
.fg
is the color for foreground text (Text_color
is a good default). Sethide_cursor
to stop showing a blinking cursor (usually because you want areadonly
editor; see below). -
edit.mouse_press(state, x,y, mouse_button)
andedit.mouse_release(x,y, mouse_button)
-- call these to position the cursor or select some text. -
edit.mouse_wheel_move(state, dx,dy)
-- call this to scroll the editor in response to a mouse wheel. -
edit.keychord_press(state, chord, key, readonly)
andedit.key_release(state, key)
-- call these to perform some standard shortcuts: insert new lines, backspace/delete, zoom in/out font size, cut/copy/paste to and from the clipboard, undo/redo. Settingreadonly
will disable mutating keystrokes. -
edit.text_input(state, t)
-- call this to insert keystrokes into the buffer. -
Text.redraw_all(state)
-- call this to clear and recompute any cached state as the cursor moves and the buffer scrolls. You shouldn't need to do this for the most part, just run it after loading or modifyingstate.lines
. -
edit.update_font_height(state, font_height)
-- updates all state dependent on font height. -
edit.update(state, dt)
-- call this fromon.update
to periodically auto-save editor contents to disk. -
edit.quit(state)
-- call this fromon.quit
to ensure any final edits get saved before quitting.
If you need more precise control, look at the comment at the top of
edit.initialize_state
in edit.lua. In brief, the widget contains an array of
lines
. Positions in the buffer are described in schema-1 locations
consisting of a line
index and a code-point pos
. We may also convert them
at times to schema-2 locations consisting of a line
, screen_line
and
pos
that better indicates how long lines wrap. Schema-2 locations are never
persisted, just generated as needed from schema-1. Important schema-1
locations in the widget are cursor1
describing where text is inserted or
deleted and screen_top1
which specifies how far down the lines is currently
visible on screen.
Some constants that affect editor behavior:
-
Margin_top
,Margin_left
,Margin_right
are integers in pixel units that affect where the editor is drawn on window (it always extends to bottom of window as needed) -
Various color constants are represented as tables with r/g/b keys:
Text_color
,Cursor_color
,Highlight_color
for drawing text.
clickable buttons
There's a facility for rendering buttons and responding to events when they're clicked. It requires setting up 3 things:
- a
state
table housing all buttons. Can be the samestate
variable the text-editor widget uses, but doesn't have to be. - specifying buttons to create in
state
. This must happen either directly or indirectly withinon.draw
. - responding to clicks on buttons in
state
. This must happen either directly or indirectly withinon.mouse_press
.
The following facilities help set these things up:
-
Clear
state
at the start of each frame:state.button_handlers = {}
Don't forget to do this, or your app will get slower over time.
-
button
creates a single button. The syntax is:button(state, name, {x=..., y=..., w=..., h=..., bg={r,g,b}, icon = function({x=..., y=..., w=..., h=...}) ... end, onpress1 = ... })
Call this either directly or indirectly from
on.draw
. It will assign a rectangle with the given dimensions and trigger the provided (zero-arg)onpress1
callback when the primary mouse button is clicked within. It will also optionally paint the rectangle with the specified background colorbg
and a foreground described by theicon
callback (which will receive the same dimensions).This way you can see everything about a button in one place. Create as many buttons as you like within a single shared
state
. -
mouse_press_consumed_by_any_button(state, x,y, mouse_button)
Call this either directly or indirectly from
on.mouse_press
. It will pass on a click to any button registered instate
. It's also helpful to ensure clicks on a button don't have other effects, so I prefer the following boilerplate early inmousepressed
:if mouse_press_consumed_by_any_button(state, x,y, mouse_button) then return end
mouse primitives
-
App.mouse_move(x, y)
-- sets the current position of the mouse to (x
,y
). (Based on LÖVE.) -
App.mouse_down(mouse_button)
-- returnstrue
if the buttonmouse_button
is pressed. Seeon.mouse_press
formouse_button
codes. (Based on LÖVE.) -
App.mouse_x()
-- returns the x coordinate of the current position of the mouse. (Based on LÖVE.) -
App.mouse_y()
-- returns the x coordinate of the current position of the mouse. (Based on LÖVE.)
keyboard primitives
-
App.is_cursor_movement(key)
-- returntrue
ifkey
is a cursor movement key (arrow keys, page-up/down, home/end) -
App.cmd_down()
,App.ctrl_down
,App.alt_down()
,App.shift_down()
-- predicates for different modifier keys. -
App.any_modifier_down()
-- returnstrue
if any of the modifier keys is currently pressed. -
App.key_down(key)
-- returnstrue
if the given key is currently pressed. (Based on LÖVE.)
interacting with files
-
App.open_for_reading(filename)
-- returns a file handle that you canread()
from. Make surefilename
is an absolute path so that your app can work reliably by double-clicking on it. (Based on Lua.) -
App.open_for_writing(filename)
-- returns a file handle that you canwrite()
to. Make surefilename
is an absolute path so that your app can work reliably by double-clicking on it. (Based on Lua.) -
json.encode(obj)
-- returns a JSON string for an objectobj
that will recreateobj
when passed tojson.decode
.obj
can be of most types but has some exceptions. (From json.lua.) -
json.decode(obj)
-- turns a JSON string into a Lua object. (From json.lua.) -
App.files(dir)
-- returns an unsorted array of the files and directories available underdir
. (From LÖVE.] -
App.file_info(filename)
-- returns some information aboutfilename
, particularly whether it exists (non-nil
return value) or not. (From LÖVE.] -
App.mkdir(path)
-- creates a directory. Make surepath
is absolute. (From LÖVE.] -
App.remove(filename)
-- removes a file or empty directory. Definitely make surefilename
is an absolute path. (From LÖVE.]
There's much more I could include here; check out the LÖVE manual and the Lua manual.
desiderata
-
App.get_time()
-- returns the number of seconds elapsed since some unspecified start time. (Based on LÖVE.) -
App.get_clipboard()
-- returns a string with the current clipboard contents. (Based on LÖVE.) -
App.set_clipboard(text)
-- stores the stringtext
in the clipboard. (Based on LÖVE.) -
array.find(arr, elem)
-- scan tablearr
forelem
assuming it's organized as an array (just numeric indices). -
array.any(arr, f)
-- scan tablearr
for any elements satisfying predicatef
. Return first such element orfalse
if none.
There's much more I could include here; check out the LÖVE manual and the Lua manual.
writing tests
-
App.screen.init{width=.., height=..}
-- creates a fake screen for a test -
App.screen.check(y, expected_contents, msg)
-- verifies text written to the fake screen aty
. This isn't very realistic;y
must exactly match what was displayed, and the expected contents show everything printed to thaty
in chronological order, regardless ofx
coordinate. In spite of these limitations, you can write lots of useful tests with this. -
App.run_after_textinput(t)
-- mimics keystrokes resulting int
and then draws one frame. -
App.run_after_keychord(chord, key)
-- mimics the finalkey
press resulting inchord
and then draws one frame. -
App.run_after_mouse_press(x,y, mouse_button)
-- mimics a mouse press down followed by drawing a frame. -
App.run_after_mouse_release(x,y, mouse_button)
-- mimics a mouse release up followed by drawing a frame. -
App.run_after_mouse_click(x,y, mouse_button)
-- mimics a mouse press down and mouse release up followed by drawing a frame. -
App.wait_fake_time(t)
-- simulates the passage of time forApp.getTime()
.
The freewheeling protocol with the driver
Freewheeling apps currently respond to the following commands from the driver:
QUIT
-- tells the current app to quitRESTART
-- tells the current app to reinitialize after saving any settingsMANIFEST
-- 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 aname
.