Apps can be composed of a wide variety of building blocks that you can use in your functions, including a small number of functions that get automatically called for you as appropriate.
-
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.
-
App.initialize_globals()
-- called before running each test and also before the app starts up. As the name suggests, use this to initialize all your global variables to something consistent. I also find it useful to be able to see all my global variables in one place, and avoid defining top-level variables anywhere else (unless they're constants and never going to be modified). -
App.initialize(arg)
-- called when app starts up afterApp.initialize_globals
. Provides inarg
an array of words typed in if you ran it from a terminal window. (Based on LÖVE.) -
App.quit()
-- called before the app shuts down. (Based on LÖVE.) -
App.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.) -
App.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 callingApp.resize
. (Based on LÖVE) -
App.filedropped(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.)
-
App.draw()
-- called to draw on the window, around 30 times a second. (Based on LÖVE.) -
App.update(dt)
-- called after every call toApp.draw
. Make changes to your app's variables here rather than inApp.draw
. Provides indt
the time since the previous call toApp.update
, which can be useful for things like smooth animations. (Based on LÖVE.) -
App.mousepressed(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.) -
App.mousereleased(x,y, mouse_button)
-- called when you release a mouse button. Provides the same arguments asApp.mousepressed()
above. (Based on LÖVE.) -
App.mousemoved(x,y, dx,dy, is_touch)
-- called any time the mouse moves. (Based on LÖVE.) -
App.wheelmoved(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.) -
App.mousefocus(in_focus)
-- called when the mouse pointer moves on or off the app window. (Based on LÖVE.) -
App.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.
-
App.textinput(t)
-- called when you press a key combination that yields (roughly) a printable character. For example,shift
anda
pressed together will callApp.textinput
withA
. (Based on LÖVE.) -
App.keyreleased(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.)
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.
-
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.)
-
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.
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 and line drawings 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. -
edit.draw(state)
-- call this fromApp.draw
to display the current editor state on the app window as requested in the call toedit.initialize_state
that createdstate
. -
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)
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. -
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. -
edit.update(state, dt)
-- call this fromApp.update
to periodically auto-save editor contents to disk. -
edit.quit(state)
-- call this fromApp.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) -
Drawing_padding_top
andDrawing_padding_bottom
affect spacing around drawings. -
Various color constants are represented as tables with r/g/b keys:
Text_color
,Cursor_color
,Highlight_color
for drawing text.Stroke_color
,Current_stroke_color
for line drawings.Icon_color
affects the color of the little mode icon on the top right of a drawing.Current_name_background_color
manages the color when naming points usingctrl+n
.Focus_stroke_color
affects the color of a point or line when you hover over it.Help_color
andHelp_background_color
affect the color of online help within line drawings.
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 withinApp.draw
. - responding to clicks on buttons in
state
. This must happen either directly or indirectly withinApp.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 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
App.mousepressed
. 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
-
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. SeeApp.mousepressed
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.)
-
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.)
-
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.
-
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.
-
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()
.