pothi.love/reference.md

519 lines
23 KiB
Markdown

# 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` and `height` -- integer dimensions for the app window in pixels.
* `flags` -- some properties of the app window. See [`flags` in `love.graphics.getMode`](https://love2d.org/wiki/love.window.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 in `arg` an
array of words typed in if you ran it from a terminal window.
(Based on [LÖVE](https://love2d.org/wiki/love.load).)
* `on.quit()` -- called before the app shuts down.
(Based on [LÖVE](https://love2d.org/wiki/love.quit).)
* `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](https://love2d.org/wiki/love.focus).)
* `on.resize(w,h)` -- called when you resize the app window. Provides new
window dimensions in `w` and `h`. Don't bother updating `App.screen.width`
and `App.screen.height`, that will happen automatically before calling
`App.resize`.
(Based on [LÖVE](https://love2d.org/wiki/love.resize))
* `on.file_drop(file)` -- called when a file icon is dragged and dropped on
the app window. Provides in `file` an object representing the file that was
dropped, that will respond to the following messages:
* `file:getFilename()` returning a string name
* `file:read()` returning the entire file contents in a single string
(Based on [LÖVE](https://love2d.org/wiki/love.filedropped).)
* `on.code_change()` -- called when you make changes to the app using
[driver.love](https://git.sr.ht/~akkartik/driver.love), any time you hit
`f4` 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](https://love2d.org/wiki/love.draw).)
* `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](https://love2d.org/wiki/love.update).)
* `on.mouse_press(x,y, mouse_button)` -- 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](https://love2d.org/wiki/love.mousepressed).)
* `on.mouse_release(x,y, mouse_button)` -- called when you release a mouse
button. Provides the same arguments as `on.mouse_press()` above.
(Based on [LÖVE](https://love2d.org/wiki/love.mousereleased).)
* `on.mouse_move(x,y, dx,dy, istouch)` -- 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](https://love2d.org/wiki/love.mousemoved).)
* `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](https://love2d.org/wiki/love.wheelmoved).)
* `on.keychord_press(chord, key)` -- called when you press a key-combination.
Provides in `key` a string name for the key most recently pressed ([valid
values](https://love2d.org/wiki/KeyConstant)). 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 `App.textinput` with `A`.
(Based on [LÖVE](https://love2d.org/wiki/love.textinput).)
* `on.key_release(key)` -- called when you press a key on the keyboard.
Provides in `key` a string name for the key ([valid values](https://love2d.org/wiki/KeyConstant)).
(Based on [LÖVE](https://love2d.org/wiki/love.keyreleased), including other
variants.)
## Functions you can call
Everything in the [LÖVE](https://love2d.org/wiki/Main_Page) and
[Lua](https://www.lua.org/manual/5.1/manual.html) 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](https://love2d.org/wiki/love.window.getMode).)
* `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](https://love2d.org/wiki/love.window.setMode).)
* `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](https://love2d.org/wiki/love.window.getPosition).)
* `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](https://love2d.org/wiki/love.window.setPosition).)
### drawing to the app window
* `App.screen.print(text, x,y)` -- print the given string `text` 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](https://love2d.org/wiki/love.graphics.print).)
* `App.screen.draw(text, x,y)` -- print the given Text object `text` 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](https://love2d.org/wiki/love.graphics.draw) albeit with
restrictions; this is only for Text objects.)
* `love.graphics.getFont()` -- returns a representation of the current font.
(From [LÖVE](https://love2d.org/wiki/love.graphics.getFont).)
* `love.graphics.setFont(font)` -- switches the current font to `font`.
(From [LÖVE](https://love2d.org/wiki/love.graphics.setFont).)
* `love.graphics.newFont(filename)` -- creates a font from the given font
file.
(From [LÖVE](https://love2d.org/wiki/love.graphics.newFont), including other
variants.)
* `App.width(text)` returns the width of `text` in pixels when rendered using
the current font.
(Based on [LÖVE](https://love2d.org/wiki/Font:getWidth).)
* `App.color(color)` -- sets the current color based on the fields `r`, `g`,
`b` and `a` (for opacity) of the table `color`.
(Based on [LÖVE](https://love2d.org/wiki/love.graphics.setColor).)
* `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](https://love2d.org/wiki/love.graphics.line), 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 dimensions `width`
along the x axis and `height` 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](https://love2d.org/wiki/love.graphics.circle), including other
variants.)
* `love.graphics.circle(mode, x, y, r)` -- draws a circle using the current
color, centered at (`x`, `y`) and with radius `r`.
`mode` is a string, either `'line'` and `'fill'`.
(From [LÖVE](https://love2d.org/wiki/love.graphics.circle), 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 radius `r`.
`mode` is a string, either `'line'` and `'fill'`.
`angle1` and `angle2` are in [radians](https://en.wikipedia.org/wiki/Radian).
(From [LÖVE](https://love2d.org/wiki/love.graphics.circle), including other
variants.)
There's much more I could include here; check out [the LÖVE manual](https://love2d.org/wiki/love.graphics).
### 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_height, line_height)` --
returns an object that can be used to render an interactive editor widget
for text starting at `y=top` on the app window, between `x=left` and
`x=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
of `lines`
* `edit.quit()` -- calling this ensures any final edits are flushed to disk
before the app exits.
* `edit.draw(state, fg, hide_cursor)` -- call this from `on.draw` to display
the current editor state on the app window as requested in the call to
`edit.initialize_state` that created `state`. `fg` is the color for
foreground text (`Text_color` is a good default). Set `hide_cursor` to stop
showing a blinking cursor (usually because you want a `readonly` editor; see
below).
* `edit.mouse_press(state, x,y, mouse_button)` and `edit.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)` and `edit.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. Setting `readonly` 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 modifying
`state.lines`.
* `edit.update_font_height(state, font_height)` -- updates all state dependent
on font height.
* `edit.update(state, dt)` -- call this from `on.update` to periodically
auto-save editor contents to disk.
* `edit.quit(state)` -- call this from `on.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 same `state` 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 within `App.draw`.
- responding to clicks on buttons in `state`. This must happen either
directly or indirectly within `App.mousepressed`.
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 `App.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
color `bg` and a foreground described by the `icon` 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 `App.mousepressed`. It will
pass on a click to any button registered in `state`. It's also helpful to
ensure clicks on a button don't have other effects, so I prefer the
following boilerplate early in `mousepressed`:
```
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](https://love2d.org/wiki/love.mouse.setPosition).)
* `App.mouse_down(mouse_button)` -- returns `true` if the button
`mouse_button` is pressed. See `App.mousepressed` for `mouse_button` codes.
(Based on [LÖVE](https://love2d.org/wiki/love.mouse.isDown).)
* `App.mouse_x()` -- returns the x coordinate of the current position of the
mouse.
(Based on [LÖVE](https://love2d.org/wiki/love.mouse.getX).)
* `App.mouse_y()` -- returns the x coordinate of the current position of the
mouse.
(Based on [LÖVE](https://love2d.org/wiki/love.mouse.getY).)
### keyboard primitives
* `App.is_cursor_movement(key)` -- return `true` if `key` 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()` -- returns `true` if any of the modifier keys is
currently pressed.
* `App.key_down(key)` -- returns `true` if the given key is currently pressed.
(Based on [LÖVE](https://love2d.org/wiki/love.keyboard.isDown).)
### interacting with files
* `App.open_for_reading(filename)` -- returns a file handle that you can
[`read()`](https://www.lua.org/manual/5.1/manual.html#pdf-file:read) from.
Make sure `filename` is an absolute path so that your app can work reliably
by double-clicking on it.
(Based on [Lua](https://www.lua.org/manual/5.1/manual.html#pdf-io.open).)
* `App.open_for_writing(filename)` -- returns a file handle that you can
[`write()`](https://www.lua.org/manual/5.1/manual.html#pdf-file:write) to.
Make sure `filename` is an absolute path so that your app can work reliably
by double-clicking on it.
(Based on [Lua](https://www.lua.org/manual/5.1/manual.html#pdf-io.open).)
* `json.encode(obj)` -- returns a JSON string for an object `obj` that will
recreate `obj` when passed to `json.decode`. `obj` can be of most types but
has some exceptions.
(From [json.lua](https://github.com/rxi/json.lua).)
* `json.decode(obj)` -- turns a JSON string into a Lua object.
(From [json.lua](https://github.com/rxi/json.lua).)
* `App.files(dir)` -- returns an unsorted array of the files and directories
available under `dir`.
(From [LÖVE](https://love2d.org/wiki/love.filesystem.getDirectoryItems).]
* `App.file_info(filename)` -- returns some information about
`filename`, particularly whether it exists (non-`nil` return value) or not.
(From [LÖVE](https://love2d.org/wiki/love.filesystem.getInfo).]
* `App.mkdir(path)` -- creates a directory. Make sure `path` is absolute.
(From [LÖVE](https://love2d.org/wiki/love.filesystem.remove).]
* `App.remove(filename)` -- removes a file or empty directory. Definitely make
sure `filename` is an absolute path.
(From [LÖVE](https://love2d.org/wiki/love.filesystem.remove).]
There's much more I could include here; check out [the LÖVE manual](https://love2d.org/wiki/love.filesystem)
and [the Lua manual](https://www.lua.org/manual/5.1/manual.html#5.7).
### desiderata
* `App.get_time()` -- returns the number of seconds elapsed since some
unspecified start time.
(Based on [LÖVE](https://love2d.org/wiki/love.timer.getTime).)
* `App.get_clipboard()` -- returns a string with the current clipboard
contents.
(Based on [LÖVE](https://love2d.org/wiki/love.system.getClipboardText).)
* `App.set_clipboard(text)` -- stores the string `text` in the clipboard.
(Based on [LÖVE](https://love2d.org/wiki/love.system.setClipboardText).)
* `array.find(arr, elem)` -- scan table `arr` for `elem` assuming it's
organized as an array (just numeric indices).
* `array.any(arr, f)` -- scan table `arr` for any elements satisfying
predicate `f`. Return first such element or `false` if none.
There's much more I could include here; check out [the LÖVE manual](https://love2d.org/wiki)
and [the Lua manual](https://www.lua.org/manual/5.1/manual.html).
### 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 at `y`. This isn't very realistic; `y` must exactly match
what was displayed, and the expected contents show everything printed to
that `y` in chronological order, regardless of `x` coordinate. In spite of
these limitations, you can write lots of useful tests with this.
* `App.run_after_textinput(t)` -- mimics keystrokes resulting in `t` and then
draws one frame.
* `App.run_after_keychord(chord)` -- mimics keystrokes resulting in `chord`
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 for `App.getTime()`.
### The freewheeling protocol with client apps or a (meta) driver
This app is the driver for freewheeling apps, but it's also itself a
freewheeling app that can be modified live by a meta driver (really just a
copy of itself with a few different filenames for reading/writing messages).
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.send_definition_to_app(state)` -- send an editor buffer's contents to
the client app as a new definition. Sets `state.load_time_error` if the
client app encountered an error.
* `live.send_to_app(msg)` -- send a command to the client app. See the
protocol above for valid commands.
* `live.receive_from_app()` -- receives a message from the client app,
returning it or `nil` if no message was received. If the message is an
error, it's saved to `Load_time_error` and the call returns an empty
response `''`.
* `live.receive_run_time_error_from_app()` -- checks for any run-time errors
in the client app, and populates them in `Run_time_error`. Automatically
invoked, so you shouldn't need to call this.
* `live.get_cmd_from_buffer(buf)` -- helper to extract the first word from a
command.
Primitives for communicating with the meta-driver:
* `live.receive_from_driver()` -- looks for a message from the meta-driver,
and returns nil if there's nothing.
* `live.send_to_driver(msg)` -- sends a message to the meta-driver.
* `live.send_run_time_error_to_driver(msg)` -- sends an error to the
meta-driver. Automatically invoked by the LÖVE error handler, so you
shouldn't need to call this.
* `live.get_binding(name)` -- look up the repo for the source code for a
`name`.