diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml
index 18bfbe0d..3abe56da 100644
--- a/.github/workflows/package.yml
+++ b/.github/workflows/package.yml
@@ -58,6 +58,11 @@ jobs:
submodules: "recursive"
fetch-depth: 0
fetch-tags: true
+ - name: install just
+ uses: extractions/setup-just@v2
+ - name: Add versioninfo
+ run: |
+ just version
- name: Build bare love package
uses: love-actions/love-actions-core@v1
with:
@@ -242,7 +247,7 @@ jobs:
VER: ${{ github.ref_name }}
run: echo VER_CODE="$(echo $VERSION | sed -e 's/^v//' -e 's/\.//g')" >> $GITHUB_ENV
- name: Package for android
- uses: compy-toys/love-actions-android@v0.2.5
+ uses: compy-toys/love-actions-android@v0.2.6
with:
love-ref: "compy"
no-soft-keyboard: "enabled"
diff --git a/.luarc.json b/.luarc.json
index ecedad0a..ccc8cc99 100644
--- a/.luarc.json
+++ b/.luarc.json
@@ -17,6 +17,7 @@
"write_to_input",
"validated_input",
"stop",
+ "pause",
"continue",
// luautils
"prequire",
diff --git a/README.md b/README.md
index 08e56e21..f8cdf686 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,7 @@ a project must be selected first.
| Search definitions | Ctrl+F |
| Exit search | Esc |
| Jump to selected definition | Enter ⏎ |
+| Edit required file / return to previous | Ctrl+O |
## Projects
@@ -140,10 +141,6 @@ contents.
Write to _file_ the text supplied as the _content_ parameter.
This can be either a string, or an array of strings.
-- `runfile(file)`
-
- Run _file_ if it's a lua script.
-
## Editor
If a project is open, the files inside can be edited or new ones
@@ -164,7 +161,7 @@ To modify an existing line, navigate there with

Happy with the modifications now, we can quit by pressing
-Ctrl-Shift-Q
+Ctrl-Shift-S

diff --git a/doc/development/OOP.md b/doc/development/OOP.md
index 24cc3a92..f7e4400b 100644
--- a/doc/development/OOP.md
+++ b/doc/development/OOP.md
@@ -1,14 +1,14 @@
-### OOP
+## OOP
-Even though lua is not an object oriented language per se, it can approximate
-some OO behaviors with clever use of metatables.
+Even though lua is not an object oriented language per se, it
+can approximate some OO behaviors with clever use of metatables.
See:
- [http://lua-users.org/wiki/ObjectOrientedProgramming][oo1]
- [http://lua-users.org/wiki/ObjectOrientationTutorial][oo2]
-#### Class factory
+### Class factory
To automate this, a class factory utility was added.
@@ -20,26 +20,73 @@ local class = require('util.class')
Then it can be used in the following ways:
-- passing a constructor (record/dataclass pattern)
+#### passing a constructor (record/dataclass pattern)
```lua
A = class.create(function()
return { a = 'a' }
end
)
-local a = A() --- results in an instance with the preset values, not very useful
+--- results in an instance with the preset values
+local a = A()
B = class.create(function(x, y)
return { x = x, y = y }
end)
-local b = B(1, 2) --- results in a B instance where x = 1 and y = 2
+--- results in a B instance where x = 1 and y = 2
+local b = B(1, 2)
+```
+
+Note: in the codebase, the function is often not defined inline,
+and very likely is named `new`, _however_ be careful not to
+confuse it with the [`new` method][n]. If the function is not a
+member of the class, it's this pattern.
+
+##### late init
+
+Sometimes it's useful to extract some behavior that's required
+both on init but also later on-demand. An instance method is a
+fine solution for this, but in order to invoke it, a full-blown
+properly initialized instance is necessary, so it can't be done
+in the constructor. Also, it would be nice to not have to do it
+on each call site where an instance is created. Hence,
+`lateinit`.
+
+Let's say we have Text objects, storing text line-by-line, and
+we want to know the average line length. This of course needs to
+be calculated on first creation, and any time the text changes.
+
+```lua
+local function new(text)
+ return {
+ text = string.lines(text or '')
+ }
+end
+local function lateinit(self)
+ local n = #(self.text)
+ if n ~= 0 then
+ local lens = table.map(self.text, string.ulen)
+ local l = 0
+ for _, v in ipairs(lens) do
+ l = l + v
+ end
+ self.avg_len = l / n
+ else
+ self.avg_len = 0
+ end
+end
+--- @class Text
+--- @field text string[]
+--- @field avg_len number
+Text = class.create(new, lateinit)
```
-For more advanced use cases, it will probably be necessary to manually control the
-metatable setup, this is achieved with the
+#### `new` method
-- `new()` method
+For more advanced use cases, it will probably be necessary to
+manually control the metatable setup, this is achieved by
+defining the `new()` method on the class.
```lua
N = class.create()
@@ -61,3 +108,4 @@ local n = N({width = 80, height = 25})
[oo1]: https://archive.vn/B3buW
[oo2]: https://archive.vn/muhJx
+[n]: #new-method
diff --git a/doc/development/editor/visible.md b/doc/development/editor/visible.md
index 6bf41649..14d2ed32 100644
--- a/doc/development/editor/visible.md
+++ b/doc/development/editor/visible.md
@@ -45,6 +45,9 @@ loin█ bresaola veni | 5
son. |
```
We arrive at 'visible coords'.
+
+#### convert
+
What is `visible(3, 3)` in the others?
* `wrapped(11, 3)`
* `normal(4, 41)`
diff --git a/doc/development/keyboard.md b/doc/development/keyboard.md
new file mode 100644
index 00000000..ce67fa6c
--- /dev/null
+++ b/doc/development/keyboard.md
@@ -0,0 +1,38 @@
+
+| Label | KeyConstant | ok? |
+| :------------------- | :---------- | :--- |
+| Esc | escape | ✓ |
+| F1 | f1 | ✗ |
+| F2 | f2 | ✗ |
+| F3 | f3 | ✗ |
+| F4 | f4 | ✗ |
+| F5 | f5 | ✗ |
+| F6 | f6 | ✗ |
+| F7 | f7 | ✗ |
+| F8 | f8 | ✗ |
+| F9 | f9 | ✗ |
+| F10 | f10 | ✓ |
+| F11 | f11 | - |
+| F12 | f12 | - |
+| numlk | code 143 | ✗ |
+| Delete | delete | ✓ |
+| Tab | tab | ✓ |
+| Enter | return | ✓ |
+| Caps Lock | capslock | ✓ |
+| ⇧ Shift | lshift | ✓ |
+| Shift | rshift | ✓ |
+| Ctrl | | ✗ |
+| Alt | lalt | ✓ |
+| PauseBreak | pause | ✓ |
+| Menu | menu | ✓ |
+| Insert (Fn+F12) | insert | ✓ |
+| Scroll Lock (Fn+F10) | scrolllock | ✓ |
+| Home (Fn+Left) | home | ✓ |
+| End (Fn+Right) | end | ✓ |
+| Mute (Fn+F5) | code 164 | ✗ |
+| PgUp (Fn+Up) | pageup | ✓ |
+| PgDn (Fn+Down) | pagedown | ✓ |
+| Next (Fn+F6) | audioplay | ✓ |
+| Prev (Fn+F7) | audioprev | ✓ |
+| Next (Fn+F8) | audionext | ✓ |
+| | | |
diff --git a/doc/intro.md b/doc/intro.md
new file mode 100644
index 00000000..5dc5d240
--- /dev/null
+++ b/doc/intro.md
@@ -0,0 +1,53 @@
+# Introduction to Compy
+
+Compy is a 7'' portable educational computer with an interactive development
+environment for the [love2d](https://love2d.org) framework. It runs on top of
+Android operating system and uses the Lua programming language for the command
+line, scripting, configuration and programming. The primary interface is the
+command console, with the standard input displayed at the bottom of the screen,
+with real-time syntax highlingting (when appropriate). The standard output is
+displayed on a terminal occupying the rest of the screen, which also doubles as
+a graphical canvas. Thus, commands with both text-based or graphical output can
+be entered. Below are some invariant principles of operations:
+
+* Pointing devices (such as mice, touch pad or touch screen) might be used, but
+ are not necessary for the successful operation of Compy.
+* Apart from syntax highlighting, anything entered in the console
+ input will only take effect upon pressing the **Enter** key.
+* If the input is syntactically invalid at the time of pressing **Enter**, the
+ user can continue editing, correcting the mistake.
+* Users cannot render compy unusable from the command line. Compy can always
+ be restored to its initial state by inserting a clean _SD card_ or formatting
+ the currently inserted one. Example projects are written onto the SD card by
+ entering `example_projects()` on the command line.
+
+## Persistence
+
+All data persisted by the user is saved on the included _SD card_. It is
+organized into named _projects_, with no file system hieararchy. Technically,
+each project is a directory under `Documents/compy/projects/` on the SD card.
+Inside these directories, there is no further directory structure.
+
+Projects can be selected or, in the absence of one, created by entering
+`project("...")` in the console, with the project's name between the double
+quotes. Running the project means executing a file called "`main.lua`" in its
+directory. It can be done by entering `run()` in the console for the currently
+selected project or `run("...")` for a different project, with the name of the
+project between the double quotes.
+
+Text files, including Lua sources can be edited using the built-in text editor
+that can be started by entering `edit("...")` in the console, with the name of
+the file between the quotes. In its absence, `main.lua` will be edited.
+
+No changes to the edited file will occur unless the user presses **Enter** _and_
+the text in the edit area at the bottom of the screen is syntactically correct.
+Pressing **Enter** takes immediate effect on the SD card, there is no need to
+"save" the edited file separately.
+
+If there is no highlighted section in the edited file, the text inputed in the
+console will be appended to the end of the file. If there is a bright white
+highlight, the entered text is inserted before it. If there is a bright yellow
+highlight, the entered text replaces it.
+
+For more information, please see the documentation of the [editor](EDITOR.md)
+and the [console](../README.md).
diff --git a/doc/mermaid/editor.md b/doc/mermaid/editor.md
index 3552900b..f80d117c 100644
--- a/doc/mermaid/editor.md
+++ b/doc/mermaid/editor.md
@@ -73,7 +73,7 @@ class VisibleContent {
class VisibleStructuredContent {
text: string[]
- blocks: VisibleBlock[]
+ v_blocks: VisibleBlock[]
reverse_map: ReverseMap
range: Range?
diff --git a/src/assets/fonts/fraps.otf b/src/assets/fonts/fraps.otf
new file mode 100644
index 00000000..2660b1b9
Binary files /dev/null and b/src/assets/fonts/fraps.otf differ
diff --git a/src/conf.lua b/src/conf.lua
index c207fbd2..0e41f9a9 100644
--- a/src/conf.lua
+++ b/src/conf.lua
@@ -63,6 +63,18 @@ function love.conf(t)
love.TRACE = true
end
+ if os.getenv("COMPY_PROF") then
+ print('DEBUG: initializing profiler')
+ local frames = os.getenv("FRAMES") or 50
+ love.PROFILE = {
+ reports = {},
+ frame = 0,
+ n_frames = frames,
+ n_rows = 7,
+ fpsc = 'T_R_B'
+ }
+ end
+
t.identity = 'compy'
t.window.resizable = false
diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua
index 8cb2f51c..4a4213a0 100644
--- a/src/controller/consoleController.lua
+++ b/src/controller/consoleController.lua
@@ -35,13 +35,11 @@ function ConsoleController.new(M, main_ctrl)
local config = M.cfg
pre_env.font = config.view.font
local IC = UserInputController(M.input)
- local EC = EditorController(M.editor)
local self = setmetatable({
time = 0,
model = M,
main_ctrl = main_ctrl,
input = IC,
- editor = EC,
-- console runner env
main_env = env,
-- copy of the application's env before the prep
@@ -58,6 +56,9 @@ function ConsoleController.new(M, main_ctrl)
cfg = config
}, ConsoleController)
+ --- the editor has to know about us
+ local EC = EditorController(M.editor, self)
+ self.editor = EC
-- initialize the stub env tables
ConsoleController.prepare_env(self)
ConsoleController.prepare_project_env(self)
@@ -66,8 +67,10 @@ function ConsoleController.new(M, main_ctrl)
end
--- @param V ConsoleView
-function ConsoleController:set_view(V)
+function ConsoleController:init_view(V)
self.view = V
+ self.input:init_view(V.input)
+ self.input:update_view()
end
--- @param name string
@@ -88,21 +91,25 @@ end
--- @return boolean success
--- @return string? errmsg
local function run_user_code(f, cc, project_path)
- local G = love.graphics
+ local gfx = love.graphics
local output = cc.model.output
local env = cc:get_base_env()
- G.setCanvas(cc:get_canvas())
+ gfx.setCanvas(cc:get_canvas())
local ok, call_err
if project_path then
env = cc:get_project_env()
end
ok, call_err = pcall(f)
if project_path and ok then -- user project exec
+ if love.PROFILE then
+ love.PROFILE.frame = 0
+ love.PROFILE.report = {}
+ end
cc.main_ctrl.set_user_handlers(env['love'])
end
output:restore_main()
- G.setCanvas()
+ gfx.setCanvas()
if not ok then
local msg = LANG.get_call_error(call_err)
return false, msg
@@ -194,10 +201,11 @@ local function project_require(cc, name)
local pr_env = cc:get_project_env()
if chunk then
setfenv(chunk, pr_env)
- chunk()
+ return chunk()
else
--- hack around love.js not having the bit lib
if name == 'bit' and _G.web then
+ ---@diagnostic disable-next-line: inject-field
pr_env.bit = o_require('util.luabit')
end
end
@@ -209,7 +217,7 @@ end
function ConsoleController.prepare_env(cc)
local prepared = cc.main_env
- prepared.G = love.graphics
+ prepared.gfx = love.graphics
local P = cc.model.projects
@@ -310,18 +318,6 @@ function ConsoleController.prepare_env(cc)
end)
end
- --- @param name string
- --- @return any
- prepared.runfile = function(name)
- local code = check_open_pr(cc._readfile, cc, name)
- local chunk, err = load(code, '', 't')
- if chunk then
- chunk()
- else
- print(err)
- end
- end
-
--- @param name string
prepared.edit = function(name)
return check_open_pr(cc.edit, cc, name)
@@ -331,6 +327,28 @@ function ConsoleController.prepare_env(cc)
cc:run_project(name)
end
+ local terminal = cc.model.output.terminal
+ local compy_namespace = {
+ terminal = {
+ --- @param x number
+ --- @param y number
+ gotoxy = function(x, y)
+ return terminal:move_to(x, y)
+ end,
+ show_cursor = function()
+ return terminal:show_cursor()
+ end,
+ hide_cursor = function()
+ return terminal:hide_cursor()
+ end,
+ clear = function()
+ return terminal:clear()
+ end
+ }
+ }
+ prepared.compy = compy_namespace
+ prepared.tty = compy_namespace.terminal
+
prepared.run = prepared.run_project
prepared.eval = LANG.eval
@@ -355,7 +373,7 @@ function ConsoleController.prepare_project_env(cc)
local cfg = cc.model.cfg
---@type table
local project_env = cc:get_pre_env_c()
- project_env.G = love.graphics
+ project_env.gfx = love.graphics
project_env.require = function(name)
return project_require(cc, name)
@@ -383,7 +401,7 @@ function ConsoleController.prepare_project_env(cc)
close_project(cc)
end
- local ui_model, input_ref
+ local ui_model, ui_con, input_ref
local create_input_handle = function()
input_ref = table.new_reftable()
end
@@ -399,10 +417,13 @@ function ConsoleController.prepare_project_env(cc)
if not input_ref then return end
ui_model = UserInputModel(cfg, eval, true, prompt)
ui_model:set_text(init)
- local inp_con = UserInputController(ui_model, input_ref)
- local view = UserInputView(cfg.view, inp_con)
+ ui_con = UserInputController(ui_model, input_ref, true)
+ local view = UserInputView(cfg.view, ui_con)
+ ui_con:init_view(view)
+ ui_con:update_view()
+
love.state.user_input = {
- M = ui_model, C = inp_con, V = view
+ M = ui_model, C = ui_con, V = view
}
return input_ref
end
@@ -429,6 +450,7 @@ function ConsoleController.prepare_project_env(cc)
return
end
ui_model:set_text(content)
+ ui_con:update_view()
end
--- @param filters table
@@ -556,16 +578,17 @@ function ConsoleController:_set_base_env(t)
table.protect(t)
end
---- @param msg string?
-function ConsoleController:suspend_run(msg)
- local runner_env = self:get_project_env()
- if love.state.app_state ~= 'running' then
+function ConsoleController:suspend()
+ if love.state.app_state ~= 'snapshot' then
return
end
+ local runner_env = self:get_project_env()
Log.info('Suspending project run')
love.state.app_state = 'inspect'
+ local msg = love.state.suspend_msg
if msg then
self.input:set_error({ tostring(msg) })
+ love.state.suspend_msg = nil
end
self.model.output:invalidate_terminal()
@@ -574,6 +597,15 @@ function ConsoleController:suspend_run(msg)
self.main_ctrl.set_default_handlers(self, self.view)
end
+--- @param msg string?
+function ConsoleController:suspend_run(msg)
+ if love.state.app_state ~= 'running' then
+ return
+ end
+ love.state.app_state = 'snapshot'
+ love.state.suspend_msg = msg
+end
+
--- @param name string
--- @param play boolean
--- @return boolean success
@@ -600,6 +632,7 @@ function ConsoleController:open_project(name, play)
then
table.insert(package.loaders, 1, project_loader)
end
+ love.state.app_state = 'project_open'
end
if open then
print('Project ' .. name .. ' opened')
@@ -638,6 +671,7 @@ function ConsoleController:stop_project_run()
View.clear_snapshot()
self.main_ctrl.set_love_draw(self, self.view)
self.main_ctrl.clear_user_handlers()
+ self.main_ctrl.report()
love.state.app_state = 'project_open'
end
@@ -667,8 +701,11 @@ function ConsoleController:edit(name, state)
if ex then
text = self:_readfile(filename)
end
- love.state.prev_state = love.state.app_state
- love.state.app_state = 'editor'
+
+ if love.state.app_state ~= 'editor' then
+ love.state.prev_state = love.state.app_state
+ love.state.app_state = 'editor'
+ end
local save = function(newcontent)
return self:_writefile(filename, newcontent)
end
@@ -677,6 +714,11 @@ function ConsoleController:edit(name, state)
self.editor:restore_state(state)
end
+--- @return EditorState?
+function ConsoleController:close_buffer()
+ self.editor:close_buffer()
+end
+
--- @return EditorState?
function ConsoleController:finish_edit()
self.editor:save_state()
@@ -685,6 +727,7 @@ function ConsoleController:finish_edit()
if ok then
love.state.app_state = love.state.prev_state
love.state.prev_state = nil
+ --- TODO clear bufferlist
return self.editor:get_state()
else
print(err)
@@ -781,11 +824,13 @@ function ConsoleController:keypressed(k)
end
end
end
+ input:update_view()
end
--- @param k string
function ConsoleController:keyreleased(k)
self.input:keyreleased(k)
+ self.input:update_view()
end
--- @param x integer
diff --git a/src/controller/controller.lua b/src/controller/controller.lua
index b90cb14e..ed63a764 100644
--- a/src/controller/controller.lua
+++ b/src/controller/controller.lua
@@ -1,3 +1,4 @@
+Prof = require("controller.profiler")
require("view.view")
require("util.string.string")
@@ -8,6 +9,9 @@ local messages = {
user_break = "BREAK into program",
exit_anykey = "Press any key to exit.",
exec_error = function(err)
+ Log.error((debug.traceback(
+ "Error: " .. tostring(err), 1):gsub("\n[^\n]+$", "")
+ ))
return 'Execution error at ' .. err
end
}
@@ -39,7 +43,7 @@ local _supported = {
'touchreleased',
}
-local _C
+local _C, _mode
--- @param msg string
local function user_error_handler(msg)
@@ -56,15 +60,11 @@ end
--- @return any ...
local function wrap(f, ...)
if _G.web then
- -- local ok, r = pcall(f, ...)
- -- if not ok then
- -- user_error_handler(r)
- -- end
- -- return r
- -- return xpcall(f, user_error_handler, ...)
- --- TODO no error handling, sorry, it leads to a stack overflow
- --- in love.wasm
- return f(...)
+ local ok, r = pcall(f, ...)
+ if not ok then
+ user_error_handler(r)
+ end
+ return r
else
return xpcall(f, user_error_handler, ...)
end
@@ -102,9 +102,15 @@ local set_handlers = function(userlove)
end
-- drawing - separate table
- local draw = userlove.draw
- if draw and draw ~= View.main_draw then
- love.draw = draw
+ local udr = userlove.draw
+ local mdr = View.main_draw
+ if udr and udr ~= mdr then
+ --- @diagnostic disable-next-line: duplicate-set-field
+ local ndr = function()
+ udr()
+ View.drawFPS()
+ end
+ love.draw = ndr
user_draw = true
end
end
@@ -167,6 +173,7 @@ Controller = {
if love.DEBUG then
if k == "1" then
table.toggle(love.debug, 'show_terminal')
+ table.toggle(love.debug, 'show_buffer')
end
if k == "2" then
table.toggle(love.debug, 'show_snapshot')
@@ -339,6 +346,9 @@ Controller = {
--- @param C ConsoleController
set_love_update = function(C)
local function update(dt)
+ if love.PROFILE then
+ Prof.update()
+ end
if click_timer > 0 then
click_timer = click_timer - dt
end
@@ -366,19 +376,22 @@ Controller = {
end
click_count = 0
end
+
local ddr = View.prev_draw
local ldr = love.draw
- local ui = get_user_input()
- if ldr ~= ddr or ui then
- local function draw()
+ if ldr ~= ddr then
+ local draw = function()
if ldr then
+ gfx.push('all')
wrap(ldr)
+ gfx.pop()
end
- local user_input = get_user_input()
- if user_input then
- user_input.V:draw(user_input.C:get_input(), C.time)
+ local ui = get_user_input()
+ if ui then
+ ui.V:draw()
end
end
+
View.prev_draw = draw
love.draw = draw
end
@@ -387,9 +400,16 @@ Controller = {
local uup = Controller._userhandlers.update
if user_update and uup
then
+ if love.state.app_state == 'snapshot' then
+ gfx.captureScreenshot(function(img)
+ local snap = gfx.newImage(img)
+ View.snapshot = snap
+ C:suspend()
+ end)
+ end
wrap(uup, dt)
end
- Controller.snapshot()
+
if love.harmony then
love.harmony.timer_update(dt)
end
@@ -410,17 +430,18 @@ Controller = {
set_love_draw = function(C, CV)
local function draw()
View.draw(C, CV)
+ View.drawFPS()
end
love.draw = draw
View.prev_draw = love.draw
View.main_draw = love.draw
View.end_draw = function()
- local w, h = G.getDimensions()
- G.setColor(Color[Color.white])
- G.setFont(C.cfg.view.font)
- G.clear()
- G.printf(messages.exit_anykey, 0, h / 3, w, "center")
+ local w, h = gfx.getDimensions()
+ gfx.setColor(Color[Color.white])
+ gfx.setFont(C.cfg.view.font)
+ gfx.clear()
+ gfx.printf(messages.exit_anykey, 0, h / 3, w, "center")
end
end,
@@ -452,19 +473,13 @@ Controller = {
love.quit = quit
end,
- --- @private
- snapshot = function()
- if user_draw then
- View.snap_canvas()
- end
- end,
-
----------------
--- public ---
----------------
- --- @param C ConsoleController
- init = function(C)
- _C = C
+ --- @param CC ConsoleController
+ init = function(CC, mode)
+ _C = CC
+ _mode = mode
end,
--- @param C ConsoleController
--- @param CV ConsoleView
@@ -520,8 +535,9 @@ Controller = {
local handlers = love.handlers
handlers.keypressed = function(k)
+ --- Power shortcuts
local function quickswitch()
- if k == 'f8' then
+ if Key.ctrl() and k == 't' then
if love.state.app_state == 'running'
or love.state.app_state == 'inspect'
or love.state.app_state == 'project_open'
@@ -556,7 +572,7 @@ Controller = {
if love.state.app_state == 'running' then
C:stop_project_run()
elseif love.state.app_state == 'editor' then
- C:finish_edit()
+ C:close_buffer()
end
end
if k == "r" then
@@ -570,15 +586,44 @@ Controller = {
C:restart()
end
end
+ local function profile()
+ if Key.ctrl() and Key.alt() and k == "p" then
+ if Key.shift() then
+ Prof.stop_profiler()
+ else
+ -- Prof.start_profiler()
+ Prof.start_oneshot()
+ end
+ end
+ if k == "f10" then
+ if love.PROFILE.fpsc == 'off' then
+ love.PROFILE.fpsc = 'T_L_B'
+ elseif love.PROFILE.fpsc == 'T_L_B' then
+ love.PROFILE.fpsc = 'T_R_B'
+ elseif love.PROFILE.fpsc == 'T_R_B' then
+ love.PROFILE.fpsc = 'T_L'
+ elseif love.PROFILE.fpsc == 'T_L' then
+ love.PROFILE.fpsc = 'T_R'
+ elseif love.PROFILE.fpsc == 'T_R' then
+ love.PROFILE.fpsc = 'off'
+ end
+ end
+ end
if playback then
if love.state.app_state == 'shutdown' then
love.event.quit()
end
restart()
+ if love.PROFILE then
+ profile()
+ end
else
restart()
quickswitch()
+ if love.PROFILE then
+ profile()
+ end
project_state_change()
end
@@ -750,6 +795,7 @@ Controller = {
for _, a in pairs(_supported) do
save_if_differs(a)
end
+
save_if_differs('draw')
end,
@@ -761,4 +807,17 @@ Controller = {
Controller._userhandlers = {}
View.clear_snapshot()
end,
+
+ oneshot = function()
+ if not love.PROFILE then return end
+ Prof.start_oneshot()
+ end,
+
+ report = function()
+ if not love.PROFILE then return end
+ local report = Prof.report()
+ if report then
+ Log.debug(report)
+ end
+ end,
}
diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua
index b79d89c1..b60b92ac 100644
--- a/src/controller/editorController.lua
+++ b/src/controller/editorController.lua
@@ -6,7 +6,8 @@ require("view.input.customStatus")
local class = require('util.class')
--- @param M EditorModel
-local function new(M)
+--- @oaram CC ConsoleController
+local function new(M, CC)
return {
input = UserInputController(M.input, nil, true),
model = M,
@@ -14,6 +15,7 @@ local function new(M)
M.search,
UserInputController(M.search.input, nil, true)
),
+ console = CC,
view = nil,
mode = 'edit',
}
@@ -28,36 +30,26 @@ end
--- @field model EditorModel
--- @field input UserInputController
--- @field search SearchController
+--- @field console ConsoleController
--- @field view EditorView?
--- @field state EditorState?
--- @field mode EditorMode
----
---- @field open function
---- @field get_state function
---- @field set_state function
---- @field save_state function
---- @field restore_state function
---- @field get_clipboard function
---- @field set_clipboard function
---- @field set_mode function
---- @field get_mode function
---- @field close function
---- @field get_active_buffer function
---- @field get_input function
---- @field update_status function
---- @field textinput function
---- @field keypressed function
EditorController = class.create(new)
+--- @param v EditorView
+function EditorController:init_view(v)
+ self.view = v
+ self.input:init_view(self.view.input)
+end
--- @param name string
---- @param content string?
+--- @param content str?
--- @param save function
function EditorController:open(name, content, save)
local w = self.model.cfg.view.drawableChars
local is_lua = string.match(name, '.lua$')
local is_md = string.match(name, '.md$')
- local ch, hl, pp
+ local ch, hl, pp, tr
if is_lua then
self.input:set_eval(LuaEditorEval)
@@ -73,6 +65,9 @@ function EditorController:open(name, content, save)
pp = function(t)
return parser.pprint(t, w)
end
+ tr = function(code)
+ return parser.trunc(code, self.model.cfg.view.fold_lines)
+ end
elseif is_md then
local mdEval = MdEval(name)
hl = mdEval.highlighter
@@ -81,11 +76,58 @@ function EditorController:open(name, content, save)
self.input:set_eval(TextEval)
end
- local b = BufferModel(name, content, save, ch, hl, pp)
- self.model.buffer = b
- self.view.buffer:open(b)
+ local b = BufferModel(name, content, save, ch, hl, pp, tr)
+ self.model.buffers:push_front(b)
+ self.view:open(b)
self:update_status()
self:set_state()
+ self.input:update_view()
+end
+
+--- @private
+function EditorController:_dump_bufferlist()
+ for i, v in ipairs(self.model.buffers) do
+ Log.debug(i, v.name)
+ end
+ orig_print()
+end
+
+function EditorController:follow_require()
+ local buf = self:get_active_buffer()
+ if not buf.semantic then return end
+ local bn = buf:get_selection()
+ local reqs = buf.semantic.requires
+ local reqsel = table.find_by_v(reqs, function(r)
+ return r.block == bn
+ end)
+
+ if reqsel then
+ local name = reqsel.name
+ self.console:edit(name .. '.lua')
+ else
+ self:pop_buffer()
+ end
+end
+
+function EditorController:pop_buffer()
+ local bs = self.model.buffers
+ local n_buffers = bs:length()
+ if n_buffers < 2 then return end
+ bs:pop_front()
+ local b = bs:first()
+ self.view:get_current_buffer():open(b)
+end
+
+function EditorController:close_buffer()
+ local bs = self.model.buffers
+ local n_buffers = bs:length()
+ if n_buffers < 2 then
+ -- Log.debug('fin', n_buffers)
+ self.console:finish_edit()
+ else
+ -- Log.debug(':bd', n_buffers)
+ self:pop_buffer()
+ end
end
--- @param m EditorMode
@@ -101,10 +143,12 @@ function EditorController:set_mode(mode)
self:save_state()
end
local init_search = function()
- self:save_state()
local db = buf.semantic
- local ds = db.definitions
- self.search:load(ds)
+ if db then
+ self:save_state()
+ local ds = db.definitions
+ self.search:load(ds)
+ end
end
local current = self.mode
@@ -113,7 +157,6 @@ function EditorController:set_mode(mode)
set_reorg()
end
if mode == 'search' then
- if not buf.semantic then return end
init_search()
end
self.mode = mode
@@ -149,8 +192,10 @@ end
--- @param clipboard string?
function EditorController:set_state(clipboard)
- local buf_view_state = self.view.buffer:get_state()
+ --- TODO: multibuffer support
local buf = self:get_active_buffer()
+ local bid = buf:get_id()
+ local buf_view_state = self.view:get_buffer(bid):get_state()
if self.state then
self.state.buffer = buf_view_state
self.state.moved = buf:get_selection()
@@ -170,17 +215,19 @@ function EditorController:get_state()
end
function EditorController:save_state()
+ --- TODO: multibuffer support
self:set_state(love.system.getClipboardText())
end
--- @param state EditorState?
function EditorController:restore_state(state)
+ --- TODO: multibuffer support
if state then
local buf = self:get_active_buffer()
local sel = state.buffer.selection
local off = state.buffer.offset
buf:set_selection(sel)
- self.view.buffer:scroll_to(off)
+ self.view:get_current_buffer():scroll_to(off)
local clip = state.clipboard
if string.is_non_empty_string(clip) then
love.system.setClipboardText(clip or '')
@@ -199,7 +246,13 @@ end
--- @return BufferModel
function EditorController:get_active_buffer()
- return self.model.buffer
+ return self.model.buffers:first()
+end
+
+--- @return Id
+function EditorController:get_active_buffer_id()
+ local buf = self:get_active_buffer()
+ return buf:get_id()
end
--- @private
@@ -209,7 +262,7 @@ function EditorController:_generate_status(sel)
--- @type BufferModel
local buffer = self:get_active_buffer()
local len = buffer:get_content_length() + 1
- local bufview = self.view.buffer
+ local bufview = self.view:get_buffer(buffer:get_id())
local more = bufview.content:get_more()
local cs
local m = self.mode
@@ -232,6 +285,7 @@ end
--- @param t string
function EditorController:textinput(t)
+ self.view:update_input()
if self.mode == 'edit' then
local input = self.model.input
if input:has_error() then
@@ -319,7 +373,7 @@ function EditorController:_move_sel(dir, by, warp, moved)
local m = buf:move_selection(dir, by, warp, mv)
if m then
if mv then self.view:refresh(moved) end
- self.view.buffer:follow_selection()
+ self.view:get_current_buffer():follow_selection()
self:update_status()
end
end
@@ -329,7 +383,7 @@ end
--- @param warp boolean?
--- @param by integer?
function EditorController:_scroll(dir, warp, by)
- self.view.buffer:scroll(dir, by, warp)
+ self.view:get_current_buffer():scroll(dir, by, warp)
self:update_status()
end
@@ -408,13 +462,14 @@ function EditorController:_search_mode_keys(k)
return
end
+ self.input:update_view()
local jump = self.search:keypressed(k)
if jump then
local buf = self:get_active_buffer()
local bn = jump.block
local ln = jump.line - 1
buf:set_selection(bn)
- self.view.buffer:scroll_to_line(ln)
+ self.view:get_current_buffer():scroll_to_line(ln)
self:set_mode('edit')
self.search:clear()
end
@@ -519,7 +574,7 @@ function EditorController:_normal_mode_keys(k)
--- handlers
local function submit()
if not Key.ctrl() and not Key.shift() and Key.is_enter(k) then
- local bufv = self.view.buffer
+ local bufv = self.view:get_current_buffer()
local function go(newtext)
if bufv:is_selection_visible() then
if buf:loaded_is_sel(true) then
@@ -609,6 +664,13 @@ function EditorController:_normal_mode_keys(k)
and k == "pagedown" then
self:_scroll('down', false, 1)
end
+
+ -- step into
+ if love.DEBUG and Key.ctrl() then
+ if k == "o" then
+ self:follow_require()
+ end
+ end
end
local function clear()
if Key.ctrl() and k == "w" then
@@ -630,6 +692,7 @@ end
--- @param k string
function EditorController:keypressed(k)
+ self.input:update_view()
local mode = self.mode
if Key.ctrl() then
@@ -651,7 +714,7 @@ function EditorController:keypressed(k)
if love.debug then
local buf = self:get_active_buffer()
- local bufview = self.view.buffer
+ local bufview = self.view:get_buffer(buf:get_id())
if k == 'f5' then
if Key.ctrl() then buf:rechunk() end
bufview:refresh()
diff --git a/src/controller/profiler.lua b/src/controller/profiler.lua
new file mode 100644
index 00000000..f7aff218
--- /dev/null
+++ b/src/controller/profiler.lua
@@ -0,0 +1,68 @@
+if not love.profiler then
+ love.profiler = require('lib.profile')
+end
+
+--- Run `f` if profiling is enabled
+local guard = function(f)
+ local pr = love.PROFILE
+ if type(pr) ~= 'table' then return end
+
+ return function()
+ f()
+ end
+end
+
+local start = function(oneshot)
+ Log.debug('Starting profiler')
+ love.PROFILE.running = true
+ love.PROFILE.oneshot = oneshot
+
+ love.profiler.start()
+end
+
+local stop = function()
+ local pr = love.PROFILE
+ if type(pr) ~= 'table' then return end
+ Log.debug('Stopping profiling')
+ love.PROFILE.running = false
+ love.profiler.stop()
+end
+
+local update = function()
+ local pr = love.PROFILE
+ if pr.running then
+ local n = pr.n_frames
+ local r = pr.n_rows
+ pr.frame = pr.frame + 1
+ if pr.frame % n == 0 then
+ local rep = love.profiler.report(r)
+ Log.debug('Report:\n', rep)
+ if not pr.reports then
+ pr.reports = {}
+ end
+ table.insert(pr.reports, rep)
+ if pr.oneshot then
+ stop()
+ end
+ end
+ end
+end
+
+local report = function()
+ local r = table.clone(love.PROFILE.report)
+ love.PROFILE.report = nil
+ love.profiler.reset()
+ return r
+end
+
+local p = {
+ start_profiler = guard(start),
+ start_oneshot = guard(function()
+ start(true)
+ end),
+ stop_profiler = guard(stop),
+ update = guard(update),
+ report = guard(report),
+}
+
+return p
diff --git a/src/controller/searchController.lua b/src/controller/searchController.lua
index 090d72bf..4f549dc0 100644
--- a/src/controller/searchController.lua
+++ b/src/controller/searchController.lua
@@ -3,10 +3,11 @@ local class = require('util.class')
--- @param model Search
--- @param uic UserInputController
local function new(model, uic)
- return {
+ local self = {
model = model,
input = uic,
}
+ return self
end
--- @class SearchController
@@ -14,9 +15,16 @@ end
--- @field input UserInputController
SearchController = class.create(new)
+--- @param v SearchView
+function SearchController:init_view(v)
+ self.view = v
+ self.input:init_view(self.view.input)
+end
+
--- @param items table[]
function SearchController:load(items)
self.model:load(items)
+ self.input:update_view()
end
--- @return ResultsDTO
@@ -64,12 +72,14 @@ end
--- @param by integer?
function SearchController:_scroll(dir, warp, by)
self.model:scroll(dir, by, warp)
+ self.input:update_view()
-- self:update_status() -- TODO: show more arrows
end
--- @param k string
--- @return table? jump
function SearchController:keypressed(k)
+ self.input:update_view()
local function navigate()
-- move selection
if k == "up" then
@@ -127,6 +137,7 @@ end
--- @param t string
function SearchController:textinput(t)
+ self.input:update_view()
self.input:add_text(t)
self:update_results()
end
diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua
index e101a5cf..60885cd8 100644
--- a/src/controller/userInputController.lua
+++ b/src/controller/userInputController.lua
@@ -15,10 +15,27 @@ end
--- @class UserInputController
--- @field model UserInputModel
+--- @field view UserInputView?
--- @field result table
--- @field disable_selection boolean
UserInputController = class.create(new)
+--- @return boolean
+function UserInputController:is_oneshot()
+ return self.model.oneshot
+end
+
+--- @param v UserInputView
+function UserInputController:init_view(v)
+ self.view = v
+end
+
+function UserInputController:update_view()
+ local input = self.model:get_input()
+ local status = self:get_status()
+ self.view:render(input, status)
+end
+
---------------
-- entered --
---------------
@@ -36,6 +53,7 @@ end
--- @param t str
function UserInputController:set_text(t)
self.model:set_text(t)
+ self:update_view()
end
--- @return boolean
@@ -126,6 +144,7 @@ end
--- @return boolean
--- @return Error[]
function UserInputController:evaluate()
+ self:update_view()
return self.model:handle(true)
end
@@ -164,6 +183,7 @@ end
--- @param k string
--- @return boolean? limit
function UserInputController:keypressed(k)
+ self:update_view()
if _G.web and k == 'space' then
self:textinput(' ')
end
@@ -362,12 +382,13 @@ function UserInputController:keypressed(k)
submit()
end
-
+ self:update_view()
return ret
end
--- @param t string
function UserInputController:textinput(t)
+ self:update_view()
if self.model:has_error() then
return
end
@@ -375,11 +396,13 @@ function UserInputController:textinput(t)
return
end
self.model:add_text(t)
+ self:update_view()
end
--- @param k string
function UserInputController:keyreleased(k)
local input = self.model
+ self:update_view()
if input:has_error() then
if k == 'space' then
@@ -395,12 +418,14 @@ function UserInputController:keyreleased(k)
end
selection()
+ self:update_view()
end
---------------
-- mouse --
---------------
+--- @private
--- @param x integer
--- @param y integer
--- @return integer c
@@ -417,6 +442,7 @@ function UserInputController:_translate_to_input_grid(x, y)
return char, line
end
+--- @private
--- @param x integer
--- @param y integer
--- @param btn integer
diff --git a/src/examples/clock/README.md b/src/examples/clock/README.md
index 0ab14f54..667f5b74 100644
--- a/src/examples/clock/README.md
+++ b/src/examples/clock/README.md
@@ -6,19 +6,19 @@ In this example, we explore how to properly create a program with it's own drawi
We will be taking over the screen drawing for this simple game.
Similar to `update()`, we can override the `love.draw()` function and have the LOVE2D framework handle displaying the content we wish.
-Drawing generally follows a simple procedure: set up some values, such as what foreground and background color to use, then build up our desired image using basic elements. These are called graphics "primitives", and we can access them from the `love.graphics` table (aliased here as `G`).
+Drawing generally follows a simple procedure: set up some values, such as what foreground and background color to use, then build up our desired image using basic elements. These are called graphics "primitives", and we can access them from the `love.graphics` table (aliased here as `gfx`).
So, our example clock:
```lua
function love.draw()
- G.setColor(Color[color + Color.bright])
- G.setBackgroundColor(Color[bg_color])
- G.setFont(font)
+ gfx.setColor(Color[color + Color.bright])
+ gfx.setBackgroundColor(Color[bg_color])
+ gfx.setFont(font)
local text = getTimestamp()
local off_x = font:getWidth(text) / 2
local off_y = font:getHeight() / 2
- G.print(text, midx - off_x, midy - off_y)
+ gfx.print(text, midx - off_x, midy - off_y)
end
```
Let's see this step-by-step.
@@ -28,13 +28,13 @@ The way the `print` helper, and most graphics helpers work, is by starting drawi
So, we need to determine the half-width and half-height of our text object to correctly draw it at the center. To do this, we can use the `getWidth()` and `getHeight()` helpers.
We determined the midpoint of the screen earlier:
```lua
-width, height = G.getDimensions()
+width, height = gfx.getDimensions()
midx = width / 2
midy = height / 2
```
Armed with this, we can draw the time dead center:
```lua
-G.print(text, midx - off_x, midy - off_y)
+gfx.print(text, midx - off_x, midy - off_y)
```
#### Getting the timestamp
@@ -60,7 +60,7 @@ function setTime()
t = s + M * m + H * h
end
```
-Reading the current time is achieved by using `os.date()`, which unlike `os.time()`, allows us to specify the format of the resulting string. We want to achieve the end result of "hour:minute"second", which we could get with the format string "%H:%M:%S", but we also need these intermediate values separately, to keep time. Instead, if the special format string "*t" is passed to the function, it will return a table with the parts of the timestamp instead of a string.
+Reading the current time is achieved by using `os.date()`, which unlike `os.time()`, allows us to specify the format of the resulting stringfx. We want to achieve the end result of "hour:minute"second", which we could get with the format string "%H:%M:%S", but we also need these intermediate values separately, to keep time. Instead, if the special format string "*t" is passed to the function, it will return a table with the parts of the timestamp instead of a stringfx.
#### Timekeeping
@@ -83,11 +83,11 @@ function getTimestamp()
return string.format("%s:%s:%s", hours, minutes, seconds)
end
```
-Quick aside on format strings: for a digital clock, we usually want to always have the numbers displayed as two digits, with colons in between them. To achieve this, we `pad` all our results, then stitch them together with `string.format()`.
+Quick aside on format strings: for a digital clock, we usually want to have the numbers displayed as two digits, with colons in between them. To achieve this, we `pad` all our results, then stitch them together with `string.format()`.
In the above code, we are doing two different kinds of division.First, an integer division (`/`), going from seconds to minutes and hours, in this case we are only interested in the whole numbers, for example: 143 seconds is two full minutes and then some, but what the clock will be displaying is '02', so the remaining 23 seconds is not interesing for the minutes part.
However, for 143 minutes, the display _should_ say 23, disregarding the two full hours, we are only interested in the remainder part. We can get this value by using `math.fmod()`, in this example, `math.fmod(143, 60)`.
#### User documentation
This program displays the current time in a randomly selected color over a randomly selected background. These colors can be changed by pressing [Space] and [Shift-Space], respectively.
-Should the clock deviate from the correct time (for example, because the program run was paused), it can be reset with the [R] key.
\ No newline at end of file
+Should the clock deviate from the correct time (for example, because the program run was paused), it can be reset with the [R] key.
diff --git a/src/examples/clock/main.lua b/src/examples/clock/main.lua
index 2ec69534..b940aa84 100644
--- a/src/examples/clock/main.lua
+++ b/src/examples/clock/main.lua
@@ -1,6 +1,6 @@
-local G = love.graphics
+local gfx = love.graphics
-width, height = G.getDimensions()
+width, height = gfx.getDimensions()
midx = width / 2
midy = height / 2
@@ -22,7 +22,7 @@ setTime()
math.randomseed(os.time())
color = math.random(7)
bg_color = math.random(7)
-font = G.newFont(144)
+font = gfx.newFont(144)
local function pad(i)
return string.format("%02d", i)
@@ -36,13 +36,13 @@ function getTimestamp()
end
function love.draw()
- G.setColor(Color[color + Color.bright])
- G.setBackgroundColor(Color[bg_color])
- G.setFont(font)
+ gfx.setColor(Color[color + Color.bright])
+ gfx.setBackgroundColor(Color[bg_color])
+ gfx.setFont(font)
local text = getTimestamp()
local off_x = font:getWidth(text) / 2
local off_y = font:getHeight() / 2
- G.print(text, midx - off_x, midy - off_y)
+ gfx.print(text, midx - off_x, midy - off_y)
end
function love.update(dt)
diff --git a/src/examples/life/README.md b/src/examples/life/README.md
index 17e43eec..5e2479ed 100644
--- a/src/examples/life/README.md
+++ b/src/examples/life/README.md
@@ -10,7 +10,7 @@ We have used the screen size before, but only in a very limited capacity, to det
```lua
cell_size = 10
-screen_w, screen_h = G.getDimensions()
+screen_w, screen_h = gfx.getDimensions()
grid_w = screen_w / cell_size
grid_h = screen_h / cell_size
```
@@ -250,10 +250,10 @@ function drawHelp()
local right_edge = screen_w - margin
local reset_msg = "Reset: [r] key or long press"
local speed_msg = "Set speed: [+]/[-] key or drag up/down"
- G.print(reset_msg, margin, (bottom - fh) - fh)
- G.print(speed_msg, margin, bottom - fh)
+ gfx.print(reset_msg, margin, (bottom - fh) - fh)
+ gfx.print(speed_msg, margin, bottom - fh)
local speed_label = string.format("Speed: %02d", speed)
local label_w = font:getWidth(speed_label)
- G.print(speed_label, right_edge - label_w, bottom - fh)
+ gfx.print(speed_label, right_edge - label_w, bottom - fh)
end
```
diff --git a/src/examples/life/main.lua b/src/examples/life/main.lua
index 74b42f7a..ad393c99 100644
--- a/src/examples/life/main.lua
+++ b/src/examples/life/main.lua
@@ -1,12 +1,13 @@
+--- Conway's Game of Life
--- original from https://github.com/Aethelios/Conway-s-Game-of-Life-in-Lua-and-Love2D
-G = love.graphics
-G.setFont(font)
+gfx = love.graphics
+gfx.setFont(font)
fh = font:getHeight()
cell_size = 10
margin = 5
-screen_w, screen_h = G.getDimensions()
+screen_w, screen_h = gfx.getDimensions()
grid_w = screen_w / cell_size
grid_h = screen_h / cell_size
grid = {}
@@ -146,22 +147,22 @@ function drawHelp()
local right_edge = screen_w - margin
local reset_msg = "Reset: [r] key or long press"
local speed_msg = "Set speed: [+]/[-] key or drag up/down"
- G.print(reset_msg, margin, (bottom - fh) - fh)
- G.print(speed_msg, margin, bottom - fh)
+ gfx.print(reset_msg, margin, (bottom - fh) - fh)
+ gfx.print(speed_msg, margin, bottom - fh)
local speed_label = string.format("Speed: %02d", speed)
local label_w = font:getWidth(speed_label)
- G.print(speed_label, right_edge - label_w, bottom - fh)
+ gfx.print(speed_label, right_edge - label_w, bottom - fh)
end
function drawCell(x, y)
- G.setColor(.9, .9, .9)
- G.rectangle('fill',
+ gfx.setColor(.9, .9, .9)
+ gfx.rectangle('fill',
(x - 1) * cell_size,
(y - 1) * cell_size,
cell_size, cell_size)
- G.setColor(.3, .3, .3)
+ gfx.setColor(.3, .3, .3)
- G.rectangle('line',
+ gfx.rectangle('line',
(x - 1) * cell_size,
(y - 1) * cell_size,
cell_size, cell_size)
@@ -176,7 +177,7 @@ function love.draw()
end
end
- G.setColor(1, 1, 1, 0.5)
+ gfx.setColor(1, 1, 1, 0.5)
drawHelp()
end
diff --git a/src/examples/paint/README.md b/src/examples/paint/README.md
index acd44128..bfde8aa5 100644
--- a/src/examples/paint/README.md
+++ b/src/examples/paint/README.md
@@ -42,7 +42,7 @@ Building on the 16-color theme, we will divide the screen into 10 columns (8 col
Then halve these columns to get the row height. Display the selected background on a double block, with the foreground color in the middle.
```lua
-width, height = G.getDimensions()
+width, height = gfx.getDimensions()
--- color palette
block_w = width / 10
block_h = block_w / 2
@@ -142,13 +142,13 @@ In our case, we are only interested in the whole number to navigate our grid, an
local y = height - block_h
for c = 0, 7 do
local x = block_w * (c + 2)
- G.setColor(Color[c])
- G.rectangle("fill", x, y, width, block_h)
- G.setColor(Color[c + 8])
- G.rectangle("fill", x, y - block_h, width, block_h)
- G.setColor(Color[Color.white])
- G.rectangle("line", x, y, width, block_h)
- G.rectangle("line", x, y - block_h, width, block_h)
+ gfx.setColor(Color[c])
+ gfx.rectangle("fill", x, y, width, block_h)
+ gfx.setColor(Color[c + 8])
+ gfx.rectangle("fill", x, y - block_h, width, block_h)
+ gfx.setColor(Color[Color.white])
+ gfx.rectangle("line", x, y, width, block_h)
+ gfx.rectangle("line", x, y - block_h, width, block_h)
end
```
@@ -163,7 +163,7 @@ Let's see how this works. First, we set up the canvas:
```lua
can_w = width - box_w
can_h = height - pal_h - 1
-canvas = G.newCanvas(can_w, can_h)
+canvas = gfx.newCanvas(can_w, can_h)
```
The default size of a canvas would be equivalent to the screen, but we have some UI elements here, so a bit smaller makes more sense. However, this does mean we need to calculate the offsets properly when detecting clicks and displaying it.
@@ -177,12 +177,12 @@ function useCanvas(x, y, btn)
local aw = getWeight()
canvas:renderTo(function()
-- ...
- G.circle("fill", x - box_w, y, aw)
+ gfx.circle("fill", x - box_w, y, aw)
end)
end
```
-Note the _x_ coordinate, which is offset by `box_w` (the width of the side panel). When drawing, we go the opposite direction: `G.draw(canvas, box_w)`.
+Note the _x_ coordinate, which is offset by `box_w` (the width of the side panel). When drawing, we go the opposite direction: `gfx.draw(canvas, box_w)`.
### Click detection
diff --git a/src/examples/paint/main.lua b/src/examples/paint/main.lua
index dc2f19b2..01e4ef7d 100644
--- a/src/examples/paint/main.lua
+++ b/src/examples/paint/main.lua
@@ -1,4 +1,4 @@
-width, height = G.getDimensions()
+width, height = gfx.getDimensions()
--- color palette
block_w = width / 10
block_h = block_w / 2
@@ -29,7 +29,7 @@ weights = { 1, 2, 4, 5, 6, 9, 11, 13 }
--- canvas
can_w = width - box_w
can_h = height - pal_h - 1
-canvas = G.newCanvas(can_w, can_h)
+canvas = gfx.newCanvas(can_w, can_h)
--- selected
color = 0 -- black
@@ -55,42 +55,42 @@ function inWeightRange(x, y)
end
function drawBackground()
- G.setColor(Color[Color.black])
- G.rectangle("fill", 0, 0, width, height)
+ gfx.setColor(Color[Color.black])
+ gfx.rectangle("fill", 0, 0, width, height)
end
function drawPaletteOutline(y)
- G.setColor(Color[bg_color])
- G.rectangle("fill", 0, y - block_h, block_w * 2, block_h * 2)
- G.setColor(Color[Color.white])
- G.rectangle("line", 0, y - block_h, sel_w, pal_h)
- G.rectangle("line", sel_w, y - block_h, width, pal_h)
+ gfx.setColor(Color[bg_color])
+ gfx.rectangle("fill", 0, y - block_h, block_w * 2, block_h * 2)
+ gfx.setColor(Color[Color.white])
+ gfx.rectangle("line", 0, y - block_h, sel_w, pal_h)
+ gfx.rectangle("line", sel_w, y - block_h, width, pal_h)
end
function drawSelectedColor(y)
- G.setColor(Color[color])
- G.rectangle("fill", block_w / 2, y - (block_h / 2),
+ gfx.setColor(Color[color])
+ gfx.rectangle("fill", block_w / 2, y - (block_h / 2),
block_w, block_h)
-- outline
local line_color = Color.white + Color.bright
if color == line_color then
line_color = Color.black
end
- G.setColor(Color[line_color])
- G.rectangle("line", block_w / 2, y - (block_h / 2),
+ gfx.setColor(Color[line_color])
+ gfx.rectangle("line", block_w / 2, y - (block_h / 2),
block_w, block_h)
end
function drawColorBoxes(y)
for c = 0, 7 do
local x = block_w * (c + 2)
- G.setColor(Color[c])
- G.rectangle("fill", x, y, width, block_h)
- G.setColor(Color[c + 8])
- G.rectangle("fill", x, y - block_h, width, block_h)
- G.setColor(Color[Color.white])
- G.rectangle("line", x, y, width, block_h)
- G.rectangle("line", x, y - block_h, width, block_h)
+ gfx.setColor(Color[c])
+ gfx.rectangle("fill", x, y, width, block_h)
+ gfx.setColor(Color[c + 8])
+ gfx.rectangle("fill", x, y - block_h, width, block_h)
+ gfx.setColor(Color[Color.white])
+ gfx.rectangle("line", x, y, width, block_h)
+ gfx.rectangle("line", x, y - block_h, width, block_h)
end
end
@@ -102,31 +102,31 @@ function drawColorPalette()
end
function drawBrush(cx, cy)
- G.push()
- G.translate(cx, cy)
+ gfx.push()
+ gfx.translate(cx, cy)
local s = icon_d / 100 * .8
- G.scale(s, s)
- G.rotate(math.pi / 4) -- 45 degree rotation
+ gfx.scale(s, s)
+ gfx.rotate(math.pi / 4) -- 45 degree rotation
-- Draw the brush handle (wooden brown color)
- G.setColor(0.6, 0.4, 0.2)
- G.rectangle("fill", -8, -80, 16, 60)
+ gfx.setColor(0.6, 0.4, 0.2)
+ gfx.rectangle("fill", -8, -80, 16, 60)
-- Handle highlight
- G.setColor(0.8, 0.6, 0.4)
- G.rectangle("fill", -6, -75, 3, 50)
+ gfx.setColor(0.8, 0.6, 0.4)
+ gfx.rectangle("fill", -6, -75, 3, 50)
-- Metal ferrule
- G.setColor(0.7, 0.7, 0.8)
- G.rectangle("fill", -10, -25, 20, 12)
+ gfx.setColor(0.7, 0.7, 0.8)
+ gfx.rectangle("fill", -10, -25, 20, 12)
-- Ferrule shine
- G.setColor(0.9, 0.9, 1.0)
- G.rectangle("fill", -8, -24, 3, 10)
+ gfx.setColor(0.9, 0.9, 1.0)
+ gfx.rectangle("fill", -8, -24, 3, 10)
-- Bristles with smooth flame-shaped tip
- G.setColor(0.2, 0.2, 0.2)
- G.rectangle("fill", -12, -13, 24, 25)
+ gfx.setColor(0.2, 0.2, 0.2)
+ gfx.rectangle("fill", -12, -13, 24, 25)
-- Create flame tip using bezier curve
local curve = love.math.newBezierCurve(
@@ -140,38 +140,38 @@ function drawBrush(cx, cy)
)
local points = curve:render()
- G.polygon("fill", points)
+ gfx.polygon("fill", points)
- G.pop()
+ gfx.pop()
end
function drawEraser(cx, cy)
- G.push()
- G.translate(cx, cy)
+ gfx.push()
+ gfx.translate(cx, cy)
local s = icon_d / 100
- G.scale(s, s)
- G.rotate(math.pi / 4) -- 45 degree rotation
+ gfx.scale(s, s)
+ gfx.rotate(math.pi / 4) -- 45 degree rotation
-- Main eraser body (light blue)
- G.setColor(Color[Color.white])
- G.rectangle("fill", -12, -40, 24, 60)
+ gfx.setColor(Color[Color.white])
+ gfx.rectangle("fill", -12, -40, 24, 60)
-- Blue stripes running lengthwise (darker blue)
- G.setColor(Color[Color.blue])
- G.rectangle("fill", -12, -40, 6, 60)
- G.rectangle("fill", 6, -40, 6, 60)
+ gfx.setColor(Color[Color.blue])
+ gfx.rectangle("fill", -12, -40, 6, 60)
+ gfx.rectangle("fill", 6, -40, 6, 60)
-- Worn eraser tip (slightly darker)
- G.setColor(Color[Color.white + Color.bright])
- G.rectangle("fill", -12, 15, 24, 8)
+ gfx.setColor(Color[Color.white + Color.bright])
+ gfx.rectangle("fill", -12, 15, 24, 8)
-- Eraser crumbs
- G.setColor(Color[Color.white])
- G.circle("fill", 18, 25, 2)
- G.circle("fill", 22, 30, 1.5)
- G.circle("fill", 15, 32, 1)
+ gfx.setColor(Color[Color.white])
+ gfx.circle("fill", 18, 25, 2)
+ gfx.circle("fill", 22, 30, 1.5)
+ gfx.circle("fill", 15, 32, 1)
- G.pop()
+ gfx.pop()
end
-- this is a color
@@ -187,14 +187,14 @@ function drawTools()
local x = tool_midx - tb_half
local y = (i - 1) * (m_2 + tb)
if i == tool then
- G.setColor(Color[Color.black])
+ gfx.setColor(Color[Color.black])
else
- G.setColor(Color[Color.white + Color.bright])
+ gfx.setColor(Color[Color.white + Color.bright])
end
- G.rectangle("fill", x, y + m_2, tb, tb)
+ gfx.rectangle("fill", x, y + m_2, tb, tb)
- G.setColor(Color[Color.black])
- G.rectangle("line", x, y + m_2, tb, tb)
+ gfx.setColor(Color[Color.black])
+ gfx.rectangle("line", x, y + m_2, tb, tb)
local draw = tools[i]
draw(tool_midx - m_2, y + tb_half + m_4)
@@ -202,20 +202,20 @@ function drawTools()
end
function drawWeightSelector()
- G.setColor(Color[Color.white + Color.bright])
- G.rectangle("line", 0, box_h - weight_h, box_w - 1, weight_h)
+ gfx.setColor(Color[Color.white + Color.bright])
+ gfx.rectangle("line", 0, box_h - weight_h, box_w - 1, weight_h)
local h = (weight_h - (2 * margin)) / 8
local w = marg_l
for i = 0, 7 do
local y = wb_y + margin + (i * h)
local lw = i + 1
local mid = y + (h / 2)
- G.setColor(Color[Color.white + Color.bright])
- G.rectangle("fill", margin, y, w, h)
+ gfx.setColor(Color[Color.white + Color.bright])
+ gfx.rectangle("fill", margin, y, w, h)
if lw == weight then
- -- G.setColor(Color[Color.white])
- -- G.rectangle("fill", margin, y, w, h)
- G.setColor(goose)
+ -- gfx.setColor(Color[Color.white])
+ -- gfx.rectangle("fill", margin, y, w, h)
+ gfx.setColor(goose)
local rx1 = 3 * margin
local rx2 = 5 * margin
local ry1 = mid - margin
@@ -224,7 +224,7 @@ function drawWeightSelector()
local x2 = 7 * margin
local y1 = mid - m_2
local y2 = mid + m_2
- G.polygon("fill",
+ gfx.polygon("fill",
-- body
rx2, ry1,
rx1, ry1,
@@ -235,9 +235,9 @@ function drawWeightSelector()
x2, mid,
x1, y1
)
- G.setColor(Color[Color.black])
- G.setLineWidth(2)
- G.polygon("line",
+ gfx.setColor(Color[Color.black])
+ gfx.setLineWidth(2)
+ gfx.polygon("line",
-- body
rx2, ry1,
rx1, ry1,
@@ -248,22 +248,22 @@ function drawWeightSelector()
x2, mid,
x1, y1
)
- G.setLineWidth(1)
+ gfx.setLineWidth(1)
else
end
- G.setColor(Color[Color.black])
+ gfx.setColor(Color[Color.black])
local aw = weights[lw]
- G.rectangle("fill", box_w / 3, mid - (aw / 2),
+ gfx.rectangle("fill", box_w / 3, mid - (aw / 2),
box_w / 2, aw)
end
end
function drawToolbox()
--- outline
- G.setColor(Color[Color.white])
- G.rectangle("fill", 0, 0, box_w - 1, height - pal_h)
- G.setColor(Color[Color.white + Color.bright])
- G.rectangle("line", 0, 0, box_w - 1, box_h)
+ gfx.setColor(Color[Color.white])
+ gfx.rectangle("fill", 0, 0, box_w - 1, height - pal_h)
+ gfx.setColor(Color[Color.white + Color.bright])
+ gfx.rectangle("line", 0, 0, box_w - 1, box_h)
drawTools()
drawWeightSelector()
end
@@ -282,8 +282,8 @@ function drawTarget()
local x, y = love.mouse.getPosition()
if inCanvasRange(x, y) then
local aw = getWeight()
- G.setColor(Color[Color.white])
- G.circle("line", x, y, aw)
+ gfx.setColor(Color[Color.white])
+ gfx.circle("line", x, y, aw)
end
end
@@ -291,7 +291,7 @@ function love.draw()
drawBackground()
drawToolbox()
drawColorPalette()
- G.draw(canvas, box_w)
+ gfx.draw(canvas, box_w)
drawTarget()
end
@@ -327,14 +327,14 @@ function useCanvas(x, y, btn)
canvas:renderTo(function()
if btn == 1 then
if tool == 1 then
- G.setColor(Color[color])
+ gfx.setColor(Color[color])
elseif tool == 2 then
- G.setColor(Color[bg_color])
+ gfx.setColor(Color[bg_color])
end
elseif btn == 2 then
- G.setColor(Color[bg_color])
+ gfx.setColor(Color[bg_color])
end
- G.circle("fill", x - box_w, y, aw)
+ gfx.circle("fill", x - box_w, y, aw)
end)
end
diff --git a/src/examples/pong/README.md b/src/examples/pong/README.md
new file mode 100644
index 00000000..a08016e8
--- /dev/null
+++ b/src/examples/pong/README.md
@@ -0,0 +1,316 @@
+# Pong2 with square ball
+
+This example shows how to build a small
+real-time game step by step.
+It demonstrates how to structure
+a program, store all state in one table,
+update the world in small bounded time steps,
+and draw the result every frame.
+
+The goal of this example is also to show
+how discrete simulation works in practice —
+how the game world advances in small,
+fixed slices of time instead of depending
+on the speed of the device.
+
+The approach used here also helps overcome
+the limited performance
+of Compy hardware by keeping updates
+deterministic and efficient.
+
+---
+
+### 1. Files and purpose
+
+The project has three files:
+- **constants.lua** — numbers that never change:
+sizes, colors, speeds.
+- **strategy.lua** — code that decides
+how the right paddle moves.
+It can follow the ball (AI) or be controlled
+by a second player.
+- **main.lua** — the main program.
+It sets up the screen, initializes state,
+runs the update loop and draws the picture.
+
+Separating logic, constants, and behavior
+keeps the program readable and efficient.
+
+---
+
+### 2. Constants
+
+```lua
+PADDLE_WIDTH = 10
+PADDLE_HEIGHT = 60
+BALL_SIZE = 10
+AI_DEADZONE = 4
+COLOR_FG = {1, 1, 1}
+COLOR_BG = {0, 0, 0}
+```
+These numbers define proportions, not absolute pixels.
+The actual paddle and ball sizes are computed
+in main.lua from the current screen height,
+so the game scales to different displays.
+They are still read-only during the run.
+
+---
+
+### 3. Game state
+
+All moving objects and scores live in one table S:
+```lua
+S = {
+ player = { x, y, w, h, dy },
+ opp = { x, y, w, h, dy },
+ ball = { x, y, dx, dy, size },
+ playerScore = 0,
+ oppScore = 0,
+ state = "start"
+}
+```
+The program updates these values each step
+and then draws them. Keeping everything
+together makes it easy to inspect and debug.
+
+---
+
+### 4. Setting up the screen
+
+At startup, cache_dims() measures
+the screen once and stores:
+```lua
+screen_w = G.getWidth()
+screen_h = G.getHeight()
+local base_h = 480
+local scale = screen_h / base_h
+paddle_w = math.floor(PADDLE_WIDTH * scale + 0.5)
+paddle_h = math.floor(PADDLE_HEIGHT * scale + 0.5)
+ball_size = math.floor(BALL_SIZE * scale + 0.5)
+paddle_max_y = screen_h - paddle_h
+ball_max_y = screen_h - ball_size
+center_x = math.floor(screen_w / 2 + 0.5)
+```
+This way, the same proportions are kept,
+but real sizes follow the
+actual screen size.
+
+---
+
+### 5. Drawing the scene
+
+All drawing happens inside love.draw():
+ 1. Clear the screen.
+ 2. Draw the cached divider canvas.
+ 3. Draw paddles, ball, and scores.
+ 4. Draw text messages such as “Press Space”.
+
+Example:
+```lua
+function love.draw()
+ cache_dims()
+ G.clear(COLOR_BG)
+ G.setColor(COLOR_FG)
+ G.draw(CENTER_CANVAS)
+ draw_paddle(S.player)
+ draw_paddle(S.opp)
+ draw_ball(S.ball)
+ draw_scores()
+ draw_state_text()
+end
+```
+The center divider is drawn once on a canvas.
+Its segment height uses the same `ball_size`
+that was computed from the screen,
+so the whole scene stays proportional.
+Drawing only from cached data keeps rendering
+predictable even on slow devices.
+
+---
+
+### 6. Input and control
+
+The left paddle can be moved with the mouse
+or keys Q and A.
+The right paddle uses a strategy selected
+at startup:
+```lua
+strategy.set_opp_strategy("ai")
+```
+for a computer opponent, or
+```lua
+strategy.set_opp_strategy("manual")
+```
+for a second human using arrow keys.
+
+Press Space to start or restart;
+Escape to quit.Input handling
+is done via a small key_actions
+table instead of long if-chains,
+making it easy to extend for
+new game states.
+
+---
+
+### 7. The update loop
+
+`love.update(dt)` is called many times per second.
+The program measures real time since the previous
+frame and advances the simulation in small bounded
+time steps. Each integration step uses the same duration
+(`FIXED_DT`), but the total number of steps per frame
+is limited by `MAX_STEPS`. This prevents the game
+from getting stuck if one frame takes too long.
+
+```lua
+acc = acc + rdt
+while acc >= FIXED_DT and steps < MAX_STEPS do
+ step_game(FIXED_DT)
+ acc = acc - FIXED_DT
+ steps = steps + 1
+end
+```
+Originally this bounded-step loop
+was added to compensate for
+an expensive per-frame screenshot
+on Compy. After removing that feature,
+the loop is no longer strictly required
+for performance. It remains good practice,
+because random long frames can still
+happen when the garbage collector (GC) runs.
+
+`FIXED_DT` is the duration of
+one physics step (1/60 s).
+`MAX_STEPS` limits the number
+of updates per frame, ensuring
+consistent gameplay across devices.
+
+### 8. One step and ball logic
+
+Each call to step_game(dt) advances
+the world by one quantum of time:
+```lua
+update_player(dt)
+strategy.update(S, dt)
+step_ball(S.ball, dt)
+handle_score()
+```
+These operations are simple arithmetic updates,
+chosen to be fast enough for Compy’s limitations.
+The combination of short steps and minimal math
+gives smooth motion without heavy load.
+
+When checking for goals, the code compares
+`b.x + b.size` with `screen_w`,
+not with a constant, because the ball size
+was scaled earlier in `cache_dims()`
+and copied into `S.ball.size` during layout.
+
+---
+
+### 9. Handling score
+
+After each step, the game checks if a goal
+was scored or the ball left the screen:
+```lua
+function handle_score()
+ if check_scored(S.ball.x) then
+ reset_ball()
+ return true
+ end
+ if S.ball.x < 0 or
+ screen_w < S.ball.x + S.ball.size
+ then
+ reset_ball()
+ return true
+ end
+ return false
+end
+```
+This helper centralizes score handling.
+It replaces the old check_score() calls
+and ensures the ball resets correctly.
+
+---
+
+### 10. Opponent strategies
+
+`strategy.lua` defines how the right paddle
+moves.
+
+AI strategy
+```lua
+local d = (S.ball.y + S.ball.size/2) -
+ (S.opp.y + S.opp.h/2)
+if math.abs(d) > AI_DEADZONE then
+ move_paddle(S.opp, (d > 0) and 1 or -1, dt)
+end
+```
+The paddle follows the ball but pauses inside
+a small “dead zone” so it does not react instantly.
+
+Manual strategy:
+```lua
+if love.keyboard.isDown("up") then dir = -1
+elseif love.keyboard.isDown("down") then dir = 1 end
+move_paddle(S.opp, dir, dt)
+```
+Any new behavior can be added as:
+```lua
+strategy.set_opp_strategy("custom", function(S, dt)
+ -- your logic here
+end)
+```
+Because the module is separate,
+the game code stays clean.
+
+---
+
+### 11. Discrete simulation and Compy performance
+
+The discrete, bounded-step simulation
+is not only a teaching tool.
+It is also a performance solution.
+
+On Compy, frame rate and CPU speed
+can vary between devices.
+If physics were tied directly to dt,
+motion would become slower
+or faster depending on load.
+
+By processing time in small,
+bounded slices the game stays predictable
+even when rendering slows down
+on Compy devices.
+
+Originally, bounded updates were introduced
+to offset the performance loss from taking
+a full-frame screenshot each cycle.
+After that feature was removed,
+this logic became optional for speed,
+yet still valuable to absorb random long frames
+caused by the garbage collector (GC).
+
+In short, discrete time keeps the game fair
+and efficient even on limited hardware.
+
+---
+
+### 12. Common issues
+
+ * Tunneling: a fast ball may skip
+ a paddle if the time step is too large.
+ Reduce ball speed or lower `FIXED_DT`.
+ * Frame drop: if too many updates pile up,
+ the loop stops at `MAX_STEPS` and
+ the game slows slightly instead of freezing.
+ * Mixed timing: always use fixed `dt`
+ for physics and real `dt` only for animation
+ or timers.
+ * High SPEED_SCALE: makes movement faster
+ but less accurate.
+ * Bounded steps also protect the game
+ from rare GC pauses.
+ * Bounded steps also protect the game
+ from rare GC pauses that can freeze
+ animation for a moment.
diff --git a/src/examples/pong/constants.lua b/src/examples/pong/constants.lua
new file mode 100644
index 00000000..1d240a57
--- /dev/null
+++ b/src/examples/pong/constants.lua
@@ -0,0 +1,26 @@
+-- constants.lua
+-- static game parameters
+
+COLOR_BG = {
+ 0,
+ 0,
+ 0
+}
+COLOR_FG = {
+ 1,
+ 1,
+ 1
+}
+
+PADDLE_WIDTH = 10
+PADDLE_HEIGHT = 60
+PADDLE_SPEED = 180
+PADDLE_OFFSET_X = 0
+
+BALL_SIZE = 10
+BALL_SPEED_X = 240
+BALL_SPEED_Y = 120
+
+WIN_SCORE = 10
+SCORE_OFFSET_Y = 20
+AI_DEADZONE = 4
diff --git a/src/examples/pong/main.lua b/src/examples/pong/main.lua
new file mode 100644
index 00000000..c0b59c35
--- /dev/null
+++ b/src/examples/pong/main.lua
@@ -0,0 +1,446 @@
+-- main.lua
+
+require "constants"
+require "strategy"
+
+gfx = love.graphics
+
+-- virtual game space
+VIRTUAL_W = 640
+VIRTUAL_H = 480
+
+-- runtime configuration
+USE_FIXED = true
+FIXED_DT = 1 / 60
+MAX_STEPS = 5
+SPEED_SCALE = 1.5
+MOUSE_SENSITIVITY = 1
+
+-- runtime variables
+view_tf = nil
+screen_w, screen_h = 0, 0
+paddle_max_y = 0
+ball_max_y = 0
+
+inited = false
+mouse_enabled = false
+time_t, acc = 0, 0
+
+-- game state
+
+S = { }
+
+S.player = {
+ x = PADDLE_OFFSET_X,
+ y = 0,
+ dy = 0
+}
+S.opp = {
+ x = 0,
+ y = 0,
+ dy = 0
+}
+S.ball = {
+ x = 0,
+ y = 0,
+ dx = BALL_SPEED_X,
+ dy = BALL_SPEED_Y
+}
+S.score = {
+ player = 0,
+ opp = 0
+}
+S.state = "start"
+
+S.strategy = {
+ fn = nil,
+ text = nil
+}
+
+-- ui resources
+font = nil
+texts = { }
+center_canvas = nil
+
+-- screen helpers
+
+function update_view_transform(w, h)
+ screen_w = w
+ screen_h = h
+ local sx = w / VIRTUAL_W
+ local sy = h / VIRTUAL_H
+ view_tf = love.math.newTransform()
+ view_tf:scale(sx, sy)
+end
+
+function cache_dims()
+ local w = gfx.getWidth()
+ local h = gfx.getHeight()
+ update_view_transform(w, h)
+end
+
+function layout()
+ S.player.x = PADDLE_OFFSET_X
+ S.player.y = (VIRTUAL_H - PADDLE_HEIGHT) / 2
+ S.opp.x = (VIRTUAL_W - PADDLE_OFFSET_X) - PADDLE_WIDTH
+ S.opp.y = (VIRTUAL_H - PADDLE_HEIGHT) / 2
+ S.ball.x = (VIRTUAL_W - BALL_SIZE) / 2
+ S.ball.y = (VIRTUAL_H - BALL_SIZE) / 2
+ paddle_max_y = VIRTUAL_H - PADDLE_HEIGHT
+ ball_max_y = VIRTUAL_H - BALL_SIZE
+end
+
+-- text helpers
+
+function set_text(name, str)
+ local old = texts[name]
+ if old then
+ old:release()
+ end
+ texts[name] = gfx.newText(font, str)
+end
+
+function rebuild_score_texts()
+ set_text("score_l", tostring(S.score.player))
+ set_text("score_r", tostring(S.score.opp))
+end
+
+function rebuild_opp_texts()
+ set_text("easy", "1 Player (easy)")
+ set_text("hard", "1 Player (hard)")
+ set_text("manual", "2 Players (keyboard)")
+end
+
+-- canvas
+
+function draw_center_line()
+ local x = VIRTUAL_W / 2 - 2
+ local step = BALL_SIZE * 2
+ local y = 0
+ while y < VIRTUAL_H do
+ gfx.rectangle("fill", x, y, 4, BALL_SIZE)
+ y = y + step
+ end
+end
+
+function build_center_canvas()
+ if center_canvas then
+ center_canvas:release()
+ end
+ center_canvas = gfx.newCanvas(VIRTUAL_W, VIRTUAL_H)
+ gfx.setCanvas(center_canvas)
+ gfx.clear(0, 0, 0, 0)
+ gfx.setColor(COLOR_FG)
+ draw_center_line()
+ gfx.setCanvas()
+end
+
+-- initialization
+
+function build_static_texts()
+ font = gfx.getFont()
+ set_text("start", "Press Space to Start")
+ set_text("gameover", "Game Over")
+ rebuild_opp_texts()
+ rebuild_score_texts()
+end
+
+function set_strategy(name)
+ S.strategy.fn = strategy[name]
+ S.strategy.text = texts[name]
+end
+
+function do_init()
+ cache_dims()
+ layout()
+ build_center_canvas()
+ build_static_texts()
+ mouse_enabled = true
+ time_t = love.timer.getTime()
+ inited = true
+ set_strategy("hard")
+end
+
+function ensure_init()
+ if not inited then
+ do_init()
+ end
+end
+
+-- paddle and ball movement
+
+function clamp_paddle(p)
+ if p.y < 0 then
+ p.y = 0
+ end
+ if VIRTUAL_H < p.y + PADDLE_HEIGHT then
+ p.y = VIRTUAL_H - PADDLE_HEIGHT
+ end
+end
+
+function move_paddle(p, dir, dt)
+ p.dy = PADDLE_SPEED * dir
+ p.y = p.y + p.dy * dt
+ clamp_paddle(p)
+end
+
+function check_scored(bx)
+ if bx < 0 then
+ return "opp"
+ end
+ if VIRTUAL_W < bx + BALL_SIZE then
+ return "player"
+ end
+ return nil
+end
+
+function move_ball(b, dt)
+ b.x = b.x + b.dx * dt
+ b.y = b.y + b.dy * dt
+ if b.y < 0 then
+ b.y = 0
+ b.dy = -b.dy
+ end
+ if VIRTUAL_H < b.y + BALL_SIZE then
+ b.y = VIRTUAL_H - BALL_SIZE
+ b.dy = -b.dy
+ end
+ return check_scored(b.x)
+end
+
+function bounce_ball(b)
+ if b.y < 0 then
+ b.y = 0
+ b.dy = -b.dy
+ end
+ if ball_max_y < b.y then
+ b.y = ball_max_y
+ b.dy = -b.dy
+ end
+end
+
+-- collision and score
+
+function hit_offset(b, p)
+ local pc = p.y + PADDLE_HEIGHT / 2
+ local bc = b.y + BALL_SIZE / 2
+ return (bc - pc) / (PADDLE_HEIGHT / 2)
+end
+
+function collide(b, p, off)
+ local hx1 = b.x < p.x + PADDLE_WIDTH
+ local hx2 = p.x < b.x + BALL_SIZE
+ local hy1 = b.y < p.y + PADDLE_HEIGHT
+ local hy2 = p.y < b.y + BALL_SIZE
+ if hx1 and hx2 and hy1 and hy2 then
+ b.x = p.x + off
+ b.dx = -b.dx
+ b.dy = b.dy + hit_offset(b, p) * (BALL_SPEED_Y * 0.75)
+ end
+end
+
+function scored(side)
+ local s = S.score
+ s[side] = s[side] + 1
+ rebuild_score_texts()
+ if WIN_SCORE <= s[side] then
+ S.state = "gameover"
+ love.mouse.setRelativeMode(false)
+ return true
+ end
+ return false
+end
+
+function reset_ball()
+ local b = S.ball
+ b.x = (VIRTUAL_W - BALL_SIZE) / 2
+ b.y = (VIRTUAL_H - BALL_SIZE) / 2
+ local total = S.score.player + S.score.opp
+ local dir = (total % 2 == 0) and 1 or -1
+ b.dx = dir * BALL_SPEED_X
+ b.dy = ((total % 3 - 1) * BALL_SPEED_Y) * 0.3
+end
+
+-- control and update
+
+key_actions = {
+ start = { },
+ play = { },
+ gameover = { }
+}
+
+function key_actions.start.space()
+ S.state = "play"
+ love.mouse.setRelativeMode(true)
+ reset_ball()
+end
+
+function key_actions.start.e()
+ set_strategy("easy")
+end
+
+function key_actions.start.h()
+ set_strategy("hard")
+end
+
+key_actions.start["1"] = function()
+ if S.strategy.fn ~= strategy.easy then
+ set_strategy("hard")
+ end
+end
+
+key_actions.start["2"] = function()
+ set_strategy("manual")
+end
+
+function key_actions.play.space()
+
+end
+
+function key_actions.play.r()
+ S.score.player = 0
+ S.score.opp = 0
+ rebuild_score_texts()
+ layout()
+ S.state = "start"
+ love.mouse.setRelativeMode(false)
+end
+
+key_actions.gameover.space = key_actions.play.r
+
+for name in pairs(key_actions) do
+ key_actions[name].escape = love.event.quit
+end
+
+function love.keypressed(k)
+ local group = key_actions[S.state]
+ if group and group[k] then
+ group[k]()
+ end
+end
+
+keydown = {
+ q = -1,
+ a = 1
+}
+
+function update_player(dt)
+ local dir = 0
+ for k, v in pairs(keydown) do
+ if love.keyboard.isDown(k) then
+ dir = v
+ end
+ end
+ move_paddle(S.player, dir, dt)
+end
+
+function love.mousemoved(x, y, dx, dy, t)
+ if not mouse_enabled or t
+ or S.state ~= "play"
+ then
+ return
+ end
+ local p = S.player
+ p.y = p.y + dy * MOUSE_SENSITIVITY
+ clamp_paddle(p)
+end
+
+-- main step/update
+
+function step_ball(b, dt)
+ move_ball(b, dt)
+ bounce_ball(b)
+ collide(b, S.player, PADDLE_WIDTH)
+ collide(b, S.opp, -BALL_SIZE)
+end
+
+function handle_score()
+ local side = check_scored(S.ball.x)
+ if side then
+ scored(side)
+ reset_ball()
+ return true
+ end
+ return false
+end
+
+function step_game(dt)
+ if S.state ~= "play" then
+ return
+ end
+ local sdt = dt * SPEED_SCALE
+ update_player(sdt)
+ S.strategy.fn(S, sdt)
+ step_ball(S.ball, sdt)
+ handle_score()
+end
+
+function update_fixed(rdt)
+ acc = acc + rdt
+ local steps = 0
+ while FIXED_DT <= acc and steps < MAX_STEPS do
+ step_game(FIXED_DT)
+ acc = acc - FIXED_DT
+ steps = steps + 1
+ end
+end
+
+function love.update(dt)
+ ensure_init()
+ local now = love.timer.getTime()
+ local rdt = now - time_t
+ time_t = now
+ if USE_FIXED then
+ update_fixed(rdt)
+ else
+ step_game(rdt)
+ end
+end
+
+-- drawing
+
+function draw_bg()
+ gfx.clear(COLOR_BG)
+ gfx.setColor(COLOR_FG)
+end
+
+function draw_paddle(p)
+ gfx.rectangle("fill", p.x, p.y, PADDLE_WIDTH, PADDLE_HEIGHT)
+end
+
+function draw_ball(b)
+ gfx.rectangle("fill", b.x, b.y, BALL_SIZE, BALL_SIZE)
+end
+
+function draw_scores()
+ gfx.draw(texts.score_l, VIRTUAL_W / 2 - 60, SCORE_OFFSET_Y)
+ gfx.draw(texts.score_r, VIRTUAL_W / 2 + 40, SCORE_OFFSET_Y)
+end
+
+function draw_state_text(s)
+ local t = texts[s]
+ if t then
+ gfx.draw(t, VIRTUAL_W / 2 - 40, VIRTUAL_H / 2 - 16)
+ end
+ if s == "start" and S.strategy.text then
+ gfx.draw(S.strategy.text, VIRTUAL_W / 2 - 40, VIRTUAL_H / 2)
+ end
+end
+
+function love.draw()
+ draw_bg()
+ gfx.push()
+ gfx.applyTransform(view_tf)
+ gfx.draw(center_canvas)
+ draw_paddle(S.player)
+ draw_paddle(S.opp)
+ draw_ball(S.ball)
+ draw_scores()
+ draw_state_text(S.state)
+ gfx.pop()
+end
+
+function love.resize(w, h)
+ update_view_transform(w, h)
+ build_center_canvas()
+end
diff --git a/src/examples/pong/strategy.lua b/src/examples/pong/strategy.lua
new file mode 100644
index 00000000..af99b85d
--- /dev/null
+++ b/src/examples/pong/strategy.lua
@@ -0,0 +1,41 @@
+-- strategy.lua
+-- opponent behavior module
+
+strategy = { }
+
+-- fast AI (hard)
+function strategy.hard(S, dt)
+ local c = S.opp.y + PADDLE_HEIGHT / 2
+ local by = S.ball.y + BALL_SIZE / 2
+ local d = by - c
+ if math.abs(d) < AI_DEADZONE then
+ S.opp.dy = 0
+ else
+ local dir = (0 < d) and 1 or -1
+ move_paddle(S.opp, dir, dt)
+ end
+end
+
+-- slow AI (easy)
+function strategy.easy(S, dt)
+ local c = S.opp.y + PADDLE_HEIGHT / 2
+ local by = S.ball.y + BALL_SIZE / 2
+ local d = by - c
+ if math.abs(d) < AI_DEADZONE then
+ S.opp.dy = 0
+ else
+ local dir = (0 < d) and 1 or -1
+ move_paddle(S.opp, dir, dt * 0.6)
+ end
+end
+
+-- Manual (second player)
+function strategy.manual(S, dt)
+ local dir = 0
+ if love.keyboard.isDown("up") then
+ dir = -1
+ elseif love.keyboard.isDown("down") then
+ dir = 1
+ end
+ move_paddle(S.opp, dir, dt)
+end
diff --git a/src/examples/sine/main.lua b/src/examples/sine/main.lua
index 8348f3a1..bcfcf7f7 100644
--- a/src/examples/sine/main.lua
+++ b/src/examples/sine/main.lua
@@ -1,20 +1,20 @@
-local G = love.graphics
+local gfx = love.graphics
local x0 = 0
-local xe = G.getWidth()
+local xe = gfx.getWidth()
local y0 = 0
-local ye = G.getHeight()
+local ye = gfx.getHeight()
local xh = xe / 2
local yh = ye / 2
-G.setColor(1, 1, 1, 0.5)
-G.setLineWidth(1)
-G.line(xh, y0, xh, ye)
-G.line(x0, yh, xe, yh)
+gfx.setColor(1, 1, 1, 0.5)
+gfx.setLineWidth(1)
+gfx.line(xh, y0, xh, ye)
+gfx.line(x0, yh, xe, yh)
-G.setColor(1, 0, 0)
-G.setPointSize(2)
+gfx.setColor(1, 0, 0)
+gfx.setPointSize(2)
local amp = 100
local times = 2
@@ -27,4 +27,4 @@ for x = 0, xe do
table.insert(points, y)
end
-G.points(points)
+gfx.points(points)
diff --git a/src/examples/tixy/README.md b/src/examples/tixy/README.md
index d5ac52bd..77dca387 100644
--- a/src/examples/tixy/README.md
+++ b/src/examples/tixy/README.md
@@ -132,14 +132,14 @@ end
```lua
function drawCircle(color, radius, x, y)
- G.setColor(color)
- G.circle(
+ gfx.setColor(color)
+ gfx.circle(
"fill",
x * (size + spacing) + offset,
y * (size + spacing) + offset,
radius
)
- G.circle(
+ gfx.circle(
"line",
x * (size + spacing) + offset,
y * (size + spacing) + offset,
diff --git a/src/examples/tixy/examples.lua b/src/examples/tixy/examples.lua
index 963ffdf2..618eb7f5 100644
--- a/src/examples/tixy/examples.lua
+++ b/src/examples/tixy/examples.lua
@@ -1,4 +1,4 @@
-examples = {}
+local examples = {}
function example(c, l)
table.insert(examples, {
@@ -98,3 +98,5 @@ example(
"return (x-5)^2 + (y-5)^2 - 99*sin(t)",
"create your own!"
)
+
+return examples
diff --git a/src/examples/tixy/main.lua b/src/examples/tixy/main.lua
index b89da9b8..da687455 100644
--- a/src/examples/tixy/main.lua
+++ b/src/examples/tixy/main.lua
@@ -1,10 +1,10 @@
-local G = love.graphics
+local gfx = love.graphics
math.randomseed(os.time())
-cw, ch = G.getDimensions()
+cw, ch = gfx.getDimensions()
midx = cw / 2
require("math")
-require("examples")
+examples = require("examples")
size = 28
spacing = 3
@@ -44,7 +44,6 @@ function advance()
load_example(e)
if ex_idx < #examples then
ex_idx = ex_idx + 1
- time = 0
end
end
@@ -53,7 +52,6 @@ function retreat()
local e = examples[ex_idx]
load_example(e)
ex_idx = ex_idx - 1
- time = 0
end
end
@@ -96,24 +94,25 @@ function setupTixy()
local f = loadstring(code)
if f then
setfenv(f, _G)
+ time = 0
tixy = f()
end
end
function drawBackground()
- G.setColor(colors.bg)
- G.rectangle("fill", 0, 0, cw, ch)
+ gfx.setColor(colors.bg)
+ gfx.rectangle("fill", 0, 0, cw, ch)
end
function drawCircle(color, radius, x, y)
- G.setColor(color)
- G.circle(
+ gfx.setColor(color)
+ gfx.circle(
"fill",
x * (size + spacing) + offset,
y * (size + spacing) + offset,
radius
)
- G.circle(
+ gfx.circle(
"line",
x * (size + spacing) + offset,
y * (size + spacing) + offset,
@@ -148,14 +147,14 @@ function drawOutput()
end
function drawText()
- G.setColor(colors.text)
+ gfx.setColor(colors.text)
local sof = (size / 2) + offset
local hof = sof / 2
- G.printf(legend, midx + hof, sof, midx - sof)
+ gfx.printf(legend, midx + hof, sof, midx - sof)
if showHelp then
- G.setColor(colors.help)
- G.setFont(font)
- G.printf(help, midx + hof, ch - (5 * sof), midx - sof)
+ gfx.setColor(colors.help)
+ gfx.setFont(font)
+ gfx.printf(help, midx + hof, ch - (5 * sof), midx - sof)
end
end
diff --git a/src/examples/turtle/README.md b/src/examples/turtle/README.md
index bc114c43..8704ece6 100644
--- a/src/examples/turtle/README.md
+++ b/src/examples/turtle/README.md
@@ -29,11 +29,11 @@ For the most simple example of this, let's represent the turtle with only an ell
local x_r = 15
local y_r = 20
function turtleA(x, y)
- G.ellipse("fill", x, y, x_r, y_r, 100)
+ gfx.ellipse("fill", x, y, x_r, y_r, 100)
end
function turtleB(x, y)
- G.translate(x, y)
- G.ellipse("fill", 0, 0, x_r, y_r, 100)
+ gfx.translate(x, y)
+ gfx.ellipse("fill", 0, 0, x_r, y_r, 100)
end
```
We can draw it at (x, y) either by drawing the shape to (x, y), or first translating the whole drawing to (x, y), and drawing at (0, 0). This might not seem that big of a deal in this simple case, but when the number of transformations and shapes go up, things cat get hard to track very quickly.
@@ -45,7 +45,7 @@ The way we translate this for LOVE is an "x radius" and a "y radius".
In o
Next, we are adding the turtle's head, which is in some sort of relation to it's body, but also the location where the whole drawing is.
```lua
-G.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100)
+gfx.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100)
```
Using the second method, we are able to provide the head position in "turtle coordinates".
So far, there's nothing about this we couldn't have done the other route, but let's proceed to the legs, which we want to draw at an angle. LOVE doesn't provide us any way to do this with only the ellipse function, we do need to `rotate` first.
@@ -53,15 +53,15 @@ So far, there's nothing about this we couldn't have done the other route, but le
See this condensed example:
```lua
function frontLeftLeg(x, y, x_r, y_r, leg_xr, leg_yr)
- G.setColor(Color[Color.green + Color.bright])
+ gfx.setColor(Color[Color.green + Color.bright])
--- move to the turtle's position
- G.translate(x, y)
+ gfx.translate(x, y)
--- move to where the leg attaches to the body
- G.translate(-x_r, -y_r / 2 - leg_xr)
+ gfx.translate(-x_r, -y_r / 2 - leg_xr)
--- rotate
- G.rotate(-math.pi / 4)
+ gfx.rotate(-math.pi / 4)
--- draw the leg
- G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
+ gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
end
```
@@ -77,17 +77,17 @@ You will notice that the actual code does not look like that. For one, in the le
Another, more interesting difference is the `push()` - `pop()` pairs around each leg.
```lua
--- left front leg
-G.push("all")
-G.translate(-x_r, -y_r / 2 - leg_xr)
-G.rotate(-math.pi / 4)
-G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
-G.pop()
+gfx.push("all")
+gfx.translate(-x_r, -y_r / 2 - leg_xr)
+gfx.rotate(-math.pi / 4)
+gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
+gfx.pop()
--- right front leg
-G.push("all")
-G.translate(x_r, -y_r / 2 - leg_xr)
-G.rotate(math.pi / 4)
-G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
-G.pop()
+gfx.push("all")
+gfx.translate(x_r, -y_r / 2 - leg_xr)
+gfx.rotate(math.pi / 4)
+gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
+gfx.pop()
```
Say we are done drawing the left leg, and now we want to proceed to drawing the other one. We could do the opposite transformations to go back to "zero", or transform from our current state to the desired one, but that leads to more complicated math and less readable code.
Instead, we work in stages. When done with the first leg, we can "reset" to our previous state (the "turtle coordinates"), and set up our next one again relative to the center.
diff --git a/src/examples/turtle/action.lua b/src/examples/turtle/action.lua
index e02c700d..1d62f00f 100644
--- a/src/examples/turtle/action.lua
+++ b/src/examples/turtle/action.lua
@@ -14,11 +14,11 @@ function moveRight(d)
tx = tx + (d or (2 * incr))
end
-function pause(msg)
+function pause_game(msg)
pause(msg or "user paused the game")
end
-actions = {
+local actions = {
forward = moveForward,
fd = moveForward,
back = moveBack,
@@ -27,5 +27,7 @@ actions = {
l = moveLeft,
right = moveRight,
r = moveRight,
- pause = pause
+ pause = pause_game
}
+
+return actions
diff --git a/src/examples/turtle/drawing.lua b/src/examples/turtle/drawing.lua
index 2327153f..5c5a2077 100644
--- a/src/examples/turtle/drawing.lua
+++ b/src/examples/turtle/drawing.lua
@@ -1,6 +1,6 @@
-local G = love.graphics
+gfx = love.graphics
-font = G.newFont()
+font = gfx.newFont()
bg_color = Color.black
body_color = Color.green
limb_color = body_color + Color.bright
@@ -14,45 +14,45 @@ function drawBackground(color)
if color_valid then
c = color
end
- G.setColor(Color[c])
- G.rectangle("fill", 0, 0, width, height)
+ gfx.setColor(Color[c])
+ gfx.rectangle("fill", 0, 0, width, height)
end
function drawFrontLegs(x_r, y_r, leg_xr, leg_yr)
- G.setColor(Color[limb_color])
- G.push("all")
- G.translate(-x_r, -y_r / 2 - leg_xr)
- G.rotate(-math.pi / 4)
- G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
- G.pop()
- G.push("all")
- G.translate(x_r, -y_r / 2 - leg_xr)
- G.rotate(math.pi / 4)
- G.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
- G.pop()
+ gfx.setColor(Color[limb_color])
+ gfx.push("all")
+ gfx.translate(-x_r, -y_r / 2 - leg_xr)
+ gfx.rotate(-math.pi / 4)
+ gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
+ gfx.pop()
+ gfx.push("all")
+ gfx.translate(x_r, -y_r / 2 - leg_xr)
+ gfx.rotate(math.pi / 4)
+ gfx.ellipse("fill", 0, 0, leg_xr, leg_yr, 100)
+ gfx.pop()
end
function drawHindLegs(x_r, y_r, leg_r, leg_yr)
- G.setColor(Color[limb_color])
- G.push("all")
- G.translate(-x_r, y_r / 2 + leg_r)
- G.rotate(math.pi / 4)
- G.ellipse("fill", 0, 0, leg_r, leg_yr, 100)
- G.pop()
- G.push("all")
- G.translate(x_r, y_r / 2 + leg_r)
- G.rotate(-math.pi / 4)
- G.ellipse("fill", 0, 0, leg_r, leg_yr, 100)
- G.pop()
+ gfx.setColor(Color[limb_color])
+ gfx.push("all")
+ gfx.translate(-x_r, y_r / 2 + leg_r)
+ gfx.rotate(math.pi / 4)
+ gfx.ellipse("fill", 0, 0, leg_r, leg_yr, 100)
+ gfx.pop()
+ gfx.push("all")
+ gfx.translate(x_r, y_r / 2 + leg_r)
+ gfx.rotate(-math.pi / 4)
+ gfx.ellipse("fill", 0, 0, leg_r, leg_yr, 100)
+ gfx.pop()
end
function drawBody(x_r, y_r, head_r)
--- body
- G.setColor(Color[body_color])
- G.ellipse("fill", 0, 0, x_r, y_r, 100)
+ gfx.setColor(Color[body_color])
+ gfx.ellipse("fill", 0, 0, x_r, y_r, 100)
--- head
local neck = 5
- G.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100)
+ gfx.circle("fill", 0, ((0 - y_r) - head_r) + neck, head_r, 100)
--- end
end
@@ -62,24 +62,24 @@ function drawTurtle(x, y)
local leg_yr = 10
local x_r = 15
local y_r = 20
- G.push("all")
- G.translate(x, y)
+ gfx.push("all")
+ gfx.translate(x, y)
drawFrontLegs(x_r, y_r, leg_xr, leg_yr)
drawHindLegs(x_r, y_r, leg_xr, leg_yr)
drawBody(x_r, y_r, head_r)
- G.pop()
+ gfx.pop()
end
function drawHelp()
- G.setColor(Color[Color.white])
- G.print("Press [I] to open console", 20, 20)
+ gfx.setColor(Color[Color.white])
+ gfx.print("Press [I] to open console", 20, 20)
local help = "Enter 'forward', 'back', 'left', or 'right'" ..
"to move the turtle!"
- G.print(help, 20, 50)
+ gfx.print(help, 20, 50)
end
function drawDebuginfo()
- G.setColor(Color[debug_color])
+ gfx.setColor(Color[debug_color])
local dt = string.format("Turtle position: (%d, %d)", tx, ty)
- G.print(dt, width - 200, 20)
+ gfx.print(dt, width - 200, 20)
end
diff --git a/src/examples/turtle/main.lua b/src/examples/turtle/main.lua
index 1833a4c9..7e570807 100644
--- a/src/examples/turtle/main.lua
+++ b/src/examples/turtle/main.lua
@@ -1,7 +1,7 @@
-require("action")
+actions = require("action")
require("drawing")
-width, height = love.graphics.getDimensions()
+width, height = gfx.getDimensions()
midx = width / 2
midy = height / 2
incr = 10
@@ -19,7 +19,7 @@ function eval(input)
end
function love.draw()
- G.setFont(font)
+ gfx.setFont(font)
drawBackground()
drawHelp()
drawTurtle(tx, ty)
diff --git a/src/harmony/init.lua b/src/harmony/init.lua
index 111aff9f..07518df0 100644
--- a/src/harmony/init.lua
+++ b/src/harmony/init.lua
@@ -83,10 +83,10 @@ local function new(_lock)
if love.update then love.update(dt) end
if love.graphics and love.graphics.isActive() then
- local G = love.graphics
- G.origin()
- G.clear(
- G.getBackgroundColor()
+ local gfx = love.graphics
+ gfx.origin()
+ gfx.clear(
+ gfx.getBackgroundColor()
)
if love.draw then love.draw() end
@@ -150,7 +150,7 @@ local function utils()
if not love.harmony then return end
if love.harmony.utils then return end
- G = love.graphics
+ local gfx = love.graphics
--- @param name love.Event
local love_event = function(name, ...)
@@ -182,6 +182,9 @@ local function utils()
lgui = false,
rgui = false,
}
+ local shortcuts = {
+ toggle = 'C-t'
+ }
--- @param tag string
@@ -203,7 +206,7 @@ local function utils()
FS.mkdirp(dir)
--- @param img_data love.ImageData
- G.captureScreenshot(function(img_data)
+ gfx.captureScreenshot(function(img_data)
if img_data then
local from = FS.join_path(
love.filesystem.getSaveDirectory(), fn
@@ -234,6 +237,7 @@ local function utils()
--- @field love_text function
--- @field screenshot function
--- @field release_keys function
+ --- @field shortcuts table
return {
patch_isDown = function()
local down = love.keyboard.isDown
@@ -291,6 +295,8 @@ local function utils()
release_keys = release_keys,
+ shortcuts = shortcuts,
+
screenshot = function(tag)
timer:script(function(wait)
wait(frame_time)
@@ -362,6 +368,7 @@ local function runner()
end)
scrun = coroutine.create(function()
+ -- timer = Timer.new()
for _, v in ipairs(scenarios) do
local tag = v.id
local sc = v.sc
diff --git a/src/harmony/scenarios/editor.lua b/src/harmony/scenarios/editor.lua
index c175b393..b9d1e64a 100644
--- a/src/harmony/scenarios/editor.lua
+++ b/src/harmony/scenarios/editor.lua
@@ -131,14 +131,14 @@ local function editor()
wait(.1)
h.screenshot('open')
wait(.2)
- h.love_key('f8')
+ h.love_key(h.shortcuts.toggle)
wait(2)
wait(.1)
h.screenshot('before')
wait(.2)
- h.love_key('f8')
+ h.love_key(h.shortcuts.toggle)
wait(.2)
h.love_key('C-home')
wait(.1)
@@ -199,12 +199,12 @@ local function editor()
wait(.1)
h.screenshot('edited')
wait(.2)
- h.love_key('f8')
+ h.love_key(h.shortcuts.toggle)
wait(.1)
h.screenshot('after')
wait(.4)
- h.love_key('f8')
+ h.love_key(h.shortcuts.toggle)
wait(.1)
h.love_key('C-f')
wait(.01)
@@ -232,7 +232,7 @@ local function editor()
wait(.1)
h.screenshot('edited-2')
wait(.2)
- h.love_key('f8')
+ h.love_key(h.shortcuts.toggle)
wait(.1)
h.screenshot('after-2')
diff --git a/src/lib/djot/djot.lua b/src/lib/djot/djot.lua
index f9a84eb3..3dfbc267 100644
--- a/src/lib/djot/djot.lua
+++ b/src/lib/djot/djot.lua
@@ -60,7 +60,7 @@ end
--- @param sourcepos (boolean?) if true, source positions are included in the AST
--- @param warn (function?) function that processes a warning, accepting a warning
--- object with `pos` and `message` fields.
---- @return (AST)
+--- @return (djotAST)
local function parse(input, sourcepos, warn)
local parser = Parser:new(input, warn)
return ast.to_ast(parser, sourcepos or false)
@@ -82,7 +82,7 @@ local function parse_events(input, warn)
end
--- Render a document's AST in human-readable form.
---- @param doc (AST) the AST
+--- @param doc (djotAST) the AST
--- @return (string) rendered AST
local function render_ast_pretty(doc)
local handle = StringHandle:new()
@@ -91,14 +91,14 @@ local function render_ast_pretty(doc)
end
--- Render a document's AST in JSON.
---- @param doc (AST) the AST
+--- @param doc (djotAST) the AST
--- @return (string) rendered AST (JSON string)
local function render_ast_json(doc)
return json.encode(doc) .. "\n"
end
--- Render a document as HTML.
---- @param doc (AST) the AST
+--- @param doc (djotAST) the AST
--- @return (string) rendered document (HTML string)
local function render_html(doc)
local handle = StringHandle:new()
diff --git a/src/lib/djot/djot/ast.lua b/src/lib/djot/djot/ast.lua
index cd875b5a..85cd2a2d 100644
--- a/src/lib/djot/djot/ast.lua
+++ b/src/lib/djot/djot/ast.lua
@@ -5,10 +5,10 @@
--- @field class? string
--- @field id? string
---- @class AST
+--- @class djotAST
--- @field t string tag for the node
--- @field s? string text for the node
---- @field c AST[] child node
+--- @field c djotAST[] child node
--- @field alias string
--- @field level integer
--- @field startidx integer
@@ -236,7 +236,7 @@ end, function(k) return displaykeys[k] or k end)
--- Create a new AST node.
--- @param tag (string) tag for the node
---- @return (AST) node (table)
+--- @return (djotAST) node (table)
local function new_node(tag)
local node = { t = tag, c = nil }
setmetatable(node, mt)
@@ -244,8 +244,8 @@ local function new_node(tag)
end
--- Add `child` as a child of `node`.
---- @param node (AST) node parent node
---- @param child (AST) node child node
+--- @param node (djotAST) node parent node
+--- @param child (djotAST) node child node
local function add_child(node, child)
if (not node.c) then
node.c = { child }
@@ -255,7 +255,7 @@ local function add_child(node, child)
end
--- Returns true if `node` has children.
---- @param node (AST) node to check
+--- @param node (djotAST) node to check
--- @return (boolean) true if node has children
local function has_children(node)
return (node.c and #node.c > 0)
@@ -303,8 +303,8 @@ local function copy_attributes(target, source)
end
end
---- @param targetnode (AST)
---- @param cs (AST)
+--- @param targetnode (djotAST)
+--- @param cs (djotAST)
local function insert_attributes_from_nodes(targetnode, cs)
targetnode.attr = targetnode.attr or new_attributes()
local i = 1
@@ -325,7 +325,7 @@ local function insert_attributes_from_nodes(targetnode, cs)
end
end
---- @param node (AST)
+--- @param node (djotAST)
local function make_definition_list_item(node)
node.t = "definition_list_item"
if not has_children(node) then
@@ -416,7 +416,7 @@ local function to_ast(parser, sourcepos)
local subject = parser.subject
local warn = parser.warn
if not warn then
- warn = function() end
+ warn = function(o) end
end
local sourceposmap
if sourcepos then
@@ -669,7 +669,7 @@ local function to_ast(parser, sourcepos)
elseif node.t == "attributes" then
-- parse attributes, add to last node
local tip = containers[#containers]
- --- @type AST|false
+ --- @type djotAST|false
local prevnode = has_children(tip) and tip.c[#tip.c]
if prevnode then
local endswithspace = false
@@ -987,7 +987,7 @@ end
--- Render an AST in human-readable form, with indentation
--- showing the hierarchy.
---- @param doc (AST) djot AST
+--- @param doc (djotAST) djot AST
--- @param handle (StringHandle) handle to which to write content
local function render(doc, handle)
render_node(doc, handle, 0)
diff --git a/src/lib/djot/djot/filter.lua b/src/lib/djot/djot/filter.lua
index bd28fc2b..f9fd4fd3 100644
--- a/src/lib/djot/djot/filter.lua
+++ b/src/lib/djot/djot/filter.lua
@@ -104,7 +104,7 @@ local function traverse(node, filterpart)
end
--- Apply a filter to a document.
---- @param node (AST)
+--- @param node (djotAST)
--- @param filter table the filter to apply
local function apply_filter(node, filter)
for _, filterpart in ipairs(filter) do
diff --git a/src/lib/hump/timer.lua b/src/lib/hump/timer.lua
index cd425bb8..f7f9933a 100644
--- a/src/lib/hump/timer.lua
+++ b/src/lib/hump/timer.lua
@@ -22,7 +22,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-]]--
+]] --
local Timer = {}
Timer.__index = Timer
@@ -31,219 +31,220 @@ local function _nothing_() end
local unpack = unpack or table.unpack
local function updateTimerHandle(handle, dt)
- -- handle: {
- -- time = ,
- -- after = ,
- -- during = ,
- -- limit = ,
- -- count = ,
- -- }
- handle.time = handle.time + dt
- handle.during(dt, math.max(handle.limit - handle.time, 0))
-
- while handle.time >= handle.limit and handle.count > 0 do
- if handle.after(handle.after) == false then
- handle.count = 0
- break
- end
- handle.time = handle.time - handle.limit
- handle.count = handle.count - 1
- end
+ -- handle: {
+ -- time = ,
+ -- after = ,
+ -- during = ,
+ -- limit = ,
+ -- count = ,
+ -- }
+ handle.time = handle.time + dt
+ handle.during(dt, math.max(handle.limit - handle.time, 0))
+
+ while handle.time >= handle.limit and handle.count > 0 do
+ if handle.after(handle.after) == false then
+ handle.count = 0
+ break
+ end
+ handle.time = handle.time - handle.limit
+ handle.count = handle.count - 1
+ end
end
function Timer:update(dt)
- -- timers may create new timers, which leads to undefined behavior
- -- in pairs() - so we need to put them in a different table first
- local to_update = {}
- for handle in pairs(self.functions) do
- to_update[handle] = handle
- end
-
- for handle in pairs(to_update) do
- if self.functions[handle] then
- updateTimerHandle(handle, dt)
- if handle.count == 0 then
- self.functions[handle] = nil
- end
- end
- end
+ -- timers may create new timers, which leads to undefined behavior
+ -- in pairs() - so we need to put them in a different table first
+ local to_update = {}
+ for handle in pairs(self.functions) do
+ to_update[handle] = handle
+ end
+
+ for handle in pairs(to_update) do
+ if self.functions[handle] then
+ updateTimerHandle(handle, dt)
+ if handle.count == 0 then
+ self.functions[handle] = nil
+ end
+ end
+ end
end
function Timer:during(delay, during, after)
- local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
- self.functions[handle] = true
- return handle
+ local handle = { time = 0, during = during, after = after or _nothing_, limit = delay, count = 1 }
+ self.functions[handle] = true
+ return handle
end
function Timer:after(delay, func)
- return self:during(delay, _nothing_, func)
+ return self:during(delay, _nothing_, func)
end
function Timer:every(delay, after, count)
- local count = count or math.huge -- exploit below: math.huge - 1 = math.huge
- local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
- self.functions[handle] = true
- return handle
+ local count = count or math.huge -- exploit below: math.huge - 1 = math.huge
+ local handle = { time = 0, during = _nothing_, after = after, limit = delay, count = count }
+ self.functions[handle] = true
+ return handle
end
function Timer:cancel(handle)
- self.functions[handle] = nil
+ self.functions[handle] = nil
end
function Timer:clear()
- self.functions = {}
+ self.functions = {}
end
function Timer:script(f)
- local co = coroutine.wrap(f)
- co(function(t)
- self:after(t, co)
- coroutine.yield()
- end)
+ local co = coroutine.wrap(f)
+ co(function(t)
+ self:after(t, co)
+ coroutine.yield()
+ end)
end
local function func_tween(tween, self, len, subject, target, method, after,
- setters_and_getters, ...)
- -- recursively collects fields that are defined in both subject and target into a flat list
- -- re-use of ref is confusing
- local to_func_tween = {}
- local function set_and_get(subject, k, v)
- setters_and_getters = setters_and_getters or {}
-
- local setter, getter
- if setters_and_getters[k] then
- setter, getter = unpack(setters_and_getters[k])
- else
- setter = subject['set'..k]
- getter = subject['get'..k]
+ setters_and_getters, ...)
+ -- recursively collects fields that are defined in both subject and target into a flat list
+ -- re-use of ref is confusing
+ local to_func_tween = {}
+ local function set_and_get(subject, k, v)
+ setters_and_getters = setters_and_getters or {}
+
+ local setter, getter
+ if setters_and_getters[k] then
+ setter, getter = unpack(setters_and_getters[k])
+ else
+ setter = subject['set' .. k]
+ getter = subject['get' .. k]
+ end
+ assert(setter and getter,
+ "key's value in subject is nil with no set/getter")
+
+ if to_func_tween[subject] == nil then
+ to_func_tween[subject] = {}
+ end
+
+ local ref = { getter(subject) }
+ to_func_tween[subject][k] = { ref, setter }
+ if type(v) == 'number' or #ref == 1 then
+ v = { v }
+ end
+ return ref, v
+ end
+
+ local function tween_collect_payload(subject, target, out)
+ for k, v in pairs(target) do
+ -- this might not be the smoothest way to do this
+ local ref = subject[k]
+ if ref == nil then
+ ref, v = set_and_get(subject, k, v)
end
- assert(setter and getter,
- "key's value in subject is nil with no set/getter")
-
- if to_func_tween[subject] == nil then
- to_func_tween[subject] = {}
- end
-
- ref = {getter(subject)}
- to_func_tween[subject][k] = {ref, setter}
- if type(v) == 'number' or #ref == 1 then
- v = {v}
- end
- return ref, v
- end
-
- local function tween_collect_payload(subject, target, out)
- for k,v in pairs(target) do
-
- -- this might not be the smoothest way to do this
- local ref = subject[k]
- if ref == nil then
- ref, v = set_and_get(subject, k, v)
- end
- assert(type(v) == type(ref), 'Type mismatch in field "'..k..'". '
- ..type(v)..' vs '.. type(ref))
- if type(v) == 'table' then
- tween_collect_payload(ref, v, out)
- else
- local ok, delta = pcall(function() return (v-ref)*1 end)
- assert(ok, 'Field "'..k..'" does not support arithmetic operations')
- out[#out+1] = {subject, k, delta}
- end
- end
- return out
- end
-
- method = tween[method or 'linear'] -- see __index
- local payload, t, args = tween_collect_payload(subject, target, {}), 0, {...}
-
- local last_s = 0
- return self:during(len, function(dt)
- t = t + dt
- local s = method(math.min(1, t/len), unpack(args))
- local ds = s - last_s
- last_s = s
- for _, info in ipairs(payload) do
- local ref, key, delta = unpack(info)
- ref[key] = ref[key] + delta * ds
+ assert(type(v) == type(ref), 'Type mismatch in field "' .. k .. '". '
+ .. type(v) .. ' vs ' .. type(ref))
+ if type(v) == 'table' then
+ tween_collect_payload(ref, v, out)
+ else
+ local ok, delta = pcall(function() return (v - ref) * 1 end)
+ assert(ok, 'Field "' .. k .. '" does not support arithmetic operations')
+ out[#out + 1] = { subject, k, delta }
end
- for ref, t in pairs(to_func_tween) do
- for key, value in pairs(t) do
- local setter_args, setter = unpack(value)
- if not pcall(function() setter(ref, unpack(setter_args)) end) then
- setter(unpack(setter_args))
- end
- end
+ end
+ return out
+ end
+
+ method = tween[method or 'linear'] -- see __index
+ local payload, t, args = tween_collect_payload(subject, target, {}), 0, { ... }
+
+ local last_s = 0
+ return self:during(len, function(dt)
+ t = t + dt
+ local s = method(math.min(1, t / len), unpack(args))
+ local ds = s - last_s
+ last_s = s
+ for _, info in ipairs(payload) do
+ local ref, key, delta = unpack(info)
+ ref[key] = ref[key] + delta * ds
+ end
+ for ref, t in pairs(to_func_tween) do
+ for key, value in pairs(t) do
+ local setter_args, setter = unpack(value)
+ if not pcall(function() setter(ref, unpack(setter_args)) end) then
+ setter(unpack(setter_args))
+ end
end
- end, after)
+ end
+ end, after)
end
local function plain_tween(tween, self, len, subject, target, method, after, ...)
- return func_tween(tween, self, len, subject, target, method, after, nil, ...)
+ return func_tween(tween, self, len, subject, target, method, after, nil, ...)
end
local function def_tween(func)
- return setmetatable(
- {
- -- helper functions
- out = function(f) -- 'rotates' a function
- return function(s, ...) return 1 - f(1-s, ...) end
- end,
- chain = function(f1, f2) -- concatenates two functions
- return function(s, ...) return (s < .5 and f1(2*s, ...) or 1 + f2(2*s-1, ...)) * .5 end
- end,
-
- -- useful tweening functions
- linear = function(s) return s end,
- quad = function(s) return s*s end,
- cubic = function(s) return s*s*s end,
- quart = function(s) return s*s*s*s end,
- quint = function(s) return s*s*s*s*s end,
- sine = function(s) return 1-math.cos(s*math.pi/2) end,
- expo = function(s) return 2^(10*(s-1)) end,
- circ = function(s) return 1 - math.sqrt(1-s*s) end,
-
- back = function(s,bounciness)
- bounciness = bounciness or 1.70158
- return s*s*((bounciness+1)*s - bounciness)
- end,
-
- bounce = function(s) -- magic numbers ahead
- local a,b = 7.5625, 1/2.75
- return math.min(a*s^2, a*(s-1.5*b)^2 + .75, a*(s-2.25*b)^2 + .9375, a*(s-2.625*b)^2 + .984375)
- end,
-
- elastic = function(s, amp, period)
- amp, period = amp and math.max(1, amp) or 1, period or .3
- return (-amp * math.sin(2*math.pi/period * (s-1) - math.asin(1/amp))) * 2^(10*(s-1))
- end,
-
-
- }, {
-
- -- register new tween
- __call = func,
-
- -- fetches function and generated compositions for method `key`
- __index = function(tweens, key)
- if type(key) == 'function' then return key end
-
- assert(type(key) == 'string', 'Method must be function or string.')
- if rawget(tweens, key) then return rawget(tweens, key) end
-
- local function construct(pattern, f)
- local method = rawget(tweens, key:match(pattern))
- if method then return f(method) end
- return nil
- end
-
- local out, chain = rawget(tweens,'out'), rawget(tweens,'chain')
- return construct('^in%-([^-]+)$', function(...) return ... end)
- or construct('^out%-([^-]+)$', out)
- or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
- or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
- or error('Unknown interpolation method: ' .. key)
- end})
+ return setmetatable(
+ {
+ -- helper functions
+ out = function(f) -- 'rotates' a function
+ return function(s, ...) return 1 - f(1 - s, ...) end
+ end,
+ chain = function(f1, f2) -- concatenates two functions
+ return function(s, ...) return (s < .5 and f1(2 * s, ...) or 1 + f2(2 * s - 1, ...)) * .5 end
+ end,
+
+ -- useful tweening functions
+ linear = function(s) return s end,
+ quad = function(s) return s * s end,
+ cubic = function(s) return s * s * s end,
+ quart = function(s) return s * s * s * s end,
+ quint = function(s) return s * s * s * s * s end,
+ sine = function(s) return 1 - math.cos(s * math.pi / 2) end,
+ expo = function(s) return 2 ^ (10 * (s - 1)) end,
+ circ = function(s) return 1 - math.sqrt(1 - s * s) end,
+
+ back = function(s, bounciness)
+ bounciness = bounciness or 1.70158
+ return s * s * ((bounciness + 1) * s - bounciness)
+ end,
+
+ bounce = function(s) -- magic numbers ahead
+ local a, b = 7.5625, 1 / 2.75
+ return math.min(a * s ^ 2, a * (s - 1.5 * b) ^ 2 + .75, a * (s - 2.25 * b) ^ 2 + .9375, a * (s - 2.625 * b) ^ 2 +
+ .984375)
+ end,
+
+ elastic = function(s, amp, period)
+ amp, period = amp and math.max(1, amp) or 1, period or .3
+ return (-amp * math.sin(2 * math.pi / period * (s - 1) - math.asin(1 / amp))) * 2 ^ (10 * (s - 1))
+ end,
+
+
+ }, {
+
+ -- register new tween
+ __call = func,
+
+ -- fetches function and generated compositions for method `key`
+ __index = function(tweens, key)
+ if type(key) == 'function' then return key end
+
+ assert(type(key) == 'string', 'Method must be function or string.')
+ if rawget(tweens, key) then return rawget(tweens, key) end
+
+ local function construct(pattern, f)
+ local method = rawget(tweens, key:match(pattern))
+ if method then return f(method) end
+ return nil
+ end
+
+ local out, chain = rawget(tweens, 'out'), rawget(tweens, 'chain')
+ return construct('^in%-([^-]+)$', function(...) return ... end)
+ or construct('^out%-([^-]+)$', out)
+ or construct('^in%-out%-([^-]+)$', function(f) return chain(f, out(f)) end)
+ or construct('^out%-in%-([^-]+)$', function(f) return chain(out(f), f) end)
+ or error('Unknown interpolation method: ' .. key)
+ end
+ })
end
@@ -252,7 +253,7 @@ Timer.func_tween = def_tween(func_tween)
-- Timer instancing
function Timer.new()
- return setmetatable({functions = {}, tween = Timer.tween}, Timer)
+ return setmetatable({ functions = {}, tween = Timer.tween }, Timer)
end
-- default instance
@@ -261,14 +262,14 @@ local default = Timer.new()
-- module forwards calls to default instance
local module = {}
for k in pairs(Timer) do
- if k ~= "__index" then
- module[k] = function(...) return default[k](default, ...) end
- end
+ if k ~= "__index" then
+ module[k] = function(...) return default[k](default, ...) end
+ end
end
module.tween = setmetatable({}, {
- __index = Timer.tween,
- __newindex = function(k,v) Timer.tween[k] = v end,
- __call = function(t, ...) return default:tween(...) end,
+ __index = Timer.tween,
+ __newindex = function(k, v) Timer.tween[k] = v end,
+ __call = function(t, ...) return default:tween(...) end,
})
-return setmetatable(module, {__call = Timer.new})
+return setmetatable(module, { __call = Timer.new })
diff --git a/src/lib/profile.lua b/src/lib/profile.lua
new file mode 100644
index 00000000..e6af5f6f
--- /dev/null
+++ b/src/lib/profile.lua
@@ -0,0 +1,222 @@
+--[[
+This file is a part of the "profile.lua" library.
+https://github.com/2dengine/profile.lua
+
+MIT License
+
+Copyright (c) 2015 2dengine LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+]]
+
+local clock = os.clock
+
+--- The "profile" module controls when to start or stop collecting data and can be used to generate reports.
+-- @module profile
+-- @alias profile
+local profile = {}
+
+-- function labels
+local _labeled = {}
+-- function definitions
+local _defined = {}
+-- time of last call
+local _tcalled = {}
+-- total execution time
+local _telapsed = {}
+-- number of calls
+local _ncalls = {}
+-- list of internal profiler functions
+local _internal = {}
+
+--- This is an internal function.
+-- @tparam string event Event type
+-- @tparam number line Line number
+-- @tparam[opt] table info Debug info table
+function profile.hooker(event, line, info)
+ info = info or debug.getinfo(2, 'fnS')
+ local f = info.func
+ -- ignore the profiler itself
+ if _internal[f] or info.what ~= "Lua" then
+ return
+ end
+ -- get the function name if available
+ if info.name then
+ _labeled[f] = info.name
+ end
+ -- find the line definition
+ if not _defined[f] then
+ _defined[f] = info.short_src .. ":" .. info.linedefined
+ _ncalls[f] = 0
+ _telapsed[f] = 0
+ end
+ if _tcalled[f] then
+ local dt = clock() - _tcalled[f]
+ _telapsed[f] = _telapsed[f] + dt
+ _tcalled[f] = nil
+ end
+ if event == "tail call" then
+ local prev = debug.getinfo(3, 'fnS')
+ profile.hooker("return", line, prev)
+ profile.hooker("call", line, info)
+ elseif event == 'call' then
+ _tcalled[f] = clock()
+ else
+ _ncalls[f] = _ncalls[f] + 1
+ end
+end
+
+--- Sets a clock function to be used by the profiler.
+-- @tparam function func Clock function that returns a number
+function profile.setclock(f)
+ assert(type(f) == "function", "clock must be a function")
+ clock = f
+end
+
+--- Starts collecting data.
+function profile.start()
+ if rawget(_G, 'jit') then
+ jit.off()
+ jit.flush()
+ end
+ debug.sethook(profile.hooker, "cr")
+end
+
+--- Stops collecting data.
+function profile.stop()
+ debug.sethook()
+ for f in pairs(_tcalled) do
+ local dt = clock() - _tcalled[f]
+ _telapsed[f] = _telapsed[f] + dt
+ _tcalled[f] = nil
+ end
+ -- merge closures
+ local lookup = {}
+ for f, d in pairs(_defined) do
+ local id = (_labeled[f] or '?') .. d
+ local f2 = lookup[id]
+ if f2 then
+ _ncalls[f2] = _ncalls[f2] + (_ncalls[f] or 0)
+ _telapsed[f2] = _telapsed[f2] + (_telapsed[f] or 0)
+ _defined[f], _labeled[f] = nil, nil
+ _ncalls[f], _telapsed[f] = nil, nil
+ else
+ lookup[id] = f
+ end
+ end
+ collectgarbage('collect')
+end
+
+--- Resets all collected data.
+function profile.reset()
+ for f in pairs(_ncalls) do
+ _ncalls[f] = 0
+ end
+ for f in pairs(_telapsed) do
+ _telapsed[f] = 0
+ end
+ for f in pairs(_tcalled) do
+ _tcalled[f] = nil
+ end
+ collectgarbage('collect')
+end
+
+--- This is an internal function.
+-- @tparam function a First function
+-- @tparam function b Second function
+-- @treturn boolean True if "a" should rank higher than "b"
+function profile.comp(a, b)
+ local dt = _telapsed[b] - _telapsed[a]
+ if dt == 0 then
+ return _ncalls[b] < _ncalls[a]
+ end
+ return dt < 0
+end
+
+--- Generates a report of functions that have been called since the profile was started.
+-- Returns the report as a numeric table of rows containing the rank, function label, number of calls, total execution time and source code line number.
+-- @tparam[opt] number limit Maximum number of rows
+-- @treturn table Table of rows
+function profile.query(limit)
+ local t = {}
+ for f, n in pairs(_ncalls) do
+ if n > 0 then
+ t[#t + 1] = f
+ end
+ end
+ table.sort(t, profile.comp)
+ if limit then
+ while #t > limit do
+ table.remove(t)
+ end
+ end
+ for i, f in ipairs(t) do
+ local dt = 0
+ if _tcalled[f] then
+ dt = clock() - _tcalled[f]
+ end
+ t[i] = { i, _labeled[f] or '?', _ncalls[f], _telapsed[f] + dt, _defined[f] }
+ end
+ return t
+end
+
+local cols = { 3, 29, 11, 24, 32 }
+
+--- Generates a text report of functions that have been called since the profile was started.
+-- Returns the report as a string that can be printed to the console.
+-- @tparam[opt] number limit Maximum number of rows
+-- @treturn string Text-based profiling report
+function profile.report(n)
+ local out = {}
+ local report = profile.query(n)
+ for i, row in ipairs(report) do
+ for j = 1, 5 do
+ local s = row[j]
+ local l2 = cols[j]
+ s = tostring(s)
+ local l1 = s:len()
+ if l1 < l2 then
+ s = s .. (' '):rep(l2 - l1)
+ elseif l1 > l2 then
+ s = s:sub(l1 - l2 + 1, l1)
+ end
+ row[j] = s
+ end
+ out[i] = table.concat(row, ' | ')
+ end
+
+ local row =
+ " +-----+-------------------------------+-------------+--------------------------+----------------------------------+ \n"
+ local col =
+ " | # | Function | Calls | Time | Code | \n"
+ local sz = row .. col .. row
+ if #out > 0 then
+ sz = sz .. ' | ' .. table.concat(out, ' | \n | ') .. ' | \n'
+ end
+ return '\n' .. sz .. row
+end
+
+-- store all internal profiler functions
+for _, v in pairs(profile) do
+ if type(v) == "function" then
+ _internal[v] = true
+ end
+end
+
+return profile
diff --git a/src/main.lua b/src/main.lua
index 5ccc3c05..aaf88408 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -1,5 +1,6 @@
local redirect_to = require("model.io.redirect")
local OS = require("util.os")
+
require("model.consoleModel")
require("controller.controller")
require("controller.consoleController")
@@ -15,7 +16,7 @@ local FS = require("util.filesystem")
require("lib.error_explorer")
-G = love.graphics
+local gfx = love.graphics
local messages = {
how_to_exit = 'Press Ctrl-Esc to exit',
@@ -45,11 +46,11 @@ local config_view = function(flags)
local font_dir = "assets/fonts/"
local mf = "ubuntu_mono_bold_nerd.ttf"
- local font_main = G.newFont(
+ local font_main = gfx.newFont(
font_dir .. mf, font_size)
- local font_icon = G.newFont(
+ local font_icon = gfx.newFont(
font_dir .. "SFMonoNerdFontMono-Regular.otf", font_size)
- local font_cjk = G.newFont(
+ local font_cjk = gfx.newFont(
font_dir .. "SarasaGothicJ-Bold.ttf", font_size * (2 / 3))
font_main:setFallbacks(font_icon, font_cjk)
@@ -64,9 +65,9 @@ local config_view = function(flags)
local lines = 16
local input_max = 14
- local font_labels = G.newFont(font_dir .. mf, 12)
- local w = love.fixWidth or G.getWidth()
- local h = love.fixHeight or G.getHeight()
+ local font_labels = gfx.newFont(font_dir .. mf, 12)
+ local w = love.fixWidth or gfx.getWidth()
+ local h = love.fixHeight or gfx.getHeight()
local eh = h - 2 * fh
local debugheight = math.floor(eh / (love.test_grid_y * fh))
local debugwidth = math.floor(w / love.test_grid_x) / fw
@@ -85,6 +86,7 @@ local config_view = function(flags)
return {
font = font_main,
iconfont = font_icon,
+ statusline_border = 4,
fh = fh,
fw = fw,
lh = lh,
@@ -104,6 +106,7 @@ local config_view = function(flags)
drawableWidth = drawableWidth,
drawableChars = drawableChars,
+ fold_lines = 1,
drawtest = tf.draw,
sizedebug = tf.size,
}
@@ -155,8 +158,7 @@ local setup_storage = function(mode)
else
if OS.get_name() == 'Android' then
if mode == 'play' then
- local savedir = love.filesystem.getSaveDirectory()
- FS.mkdirp(savedir)
+ --- initializing directory moved to app code
else
local ok, sd_path = android_storage_find()
if not ok then
@@ -187,8 +189,10 @@ local setup_storage = function(mode)
project_path = project_path,
}
for _, d in pairs(paths) do
- local ok, err = FS.mkdir(d)
- if not ok then Log(err) end
+ if mode ~= 'play' then
+ local ok, err = FS.mkdirp(d)
+ if not ok then Log(err) end
+ end
end
--- this is virtual, we don't want to actually create it
paths.play_path = '/play'
@@ -262,6 +266,15 @@ function love.load()
local autotest =
mode == 'test' and startup.testflags.auto or false
local playback = mode == 'play'
+ if love.PROFILE then
+ love.profiler = require('lib.profile')
+ end
+
+ --- @type LoveState
+ love.state = {
+ testing = false,
+ app_state = 'starting',
+ }
if playback and not string.is_non_empty_string(startup.path) then
exit(messages.play_no_project)
@@ -292,17 +305,14 @@ function love.load()
load_project(startup.path or '', paths)
end
- --- @type LoveState
- love.state = {
- testing = false,
- has_removable = has_removable,
- user_input = nil,
- app_state = 'ready'
- }
+ love.state.has_removable = has_removable
+ love.state.app_state = 'ready'
+
if love.DEBUG then
love.debug = {
- show_snapshot = true,
show_terminal = true,
+ show_buffer = true,
+ show_snapshot = true,
show_canvas = true,
show_input = true,
once = 0
@@ -336,9 +346,8 @@ function love.load()
redirect_to(CM)
local CC = ConsoleController(CM, ctrl)
local CV = ConsoleView(baseconf, CC)
- CC:set_view(CV)
- ctrl.init(CC)
+ ctrl.init(CC, mode)
ctrl.setup_callback_handlers(CC)
ctrl.set_default_handlers(CC, CV)
diff --git a/src/model/canvasModel.lua b/src/model/canvasModel.lua
index 767c99c8..e9449f01 100644
--- a/src/model/canvasModel.lua
+++ b/src/model/canvasModel.lua
@@ -4,7 +4,7 @@ require("util.view")
local class = require('util.class')
local Terminal = require("lib.terminal")
-local G = love.graphics
+local gfx = love.graphics
--- @class CanvasModel
--- @field terminal table
@@ -31,7 +31,7 @@ function CanvasModel.new(cfg)
-- h = ViewUtils.get_drawable_height(cfg.view)
h = cfg.view.h
end
- local canvas = G.newCanvas(w, h)
+ local canvas = gfx.newCanvas(w, h)
local custom_height = cfg.view.fh * cfg.view.lh
local term = Terminal(w, h, cfg.view.font,
nil, custom_height)
@@ -85,14 +85,14 @@ end
function CanvasModel:clear_canvas()
return self.canvas:renderTo(function()
- G.clear(0, 0, 0, 0)
+ gfx.clear(0, 0, 0, 0)
end)
end
function CanvasModel:draw_to()
- G.setCanvas(self.canvas)
+ gfx.setCanvas(self.canvas)
end
function CanvasModel:restore_main()
- G.setCanvas()
+ gfx.setCanvas()
end
diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua
index cac3a5c9..75e51a07 100644
--- a/src/model/editor/bufferModel.lua
+++ b/src/model/editor/bufferModel.lua
@@ -28,16 +28,22 @@ end
--- @alias Content Dequeue|Dequeue
--- @param name string
---- @param content string
+--- @param content str
--- @param save function
--- @param chunker Chunker?
--- @param highlighter Highlighter?
---- @param printer function?
+--- @param printer Printer?
+--- @param truncer function?
--- @return BufferModel?
-local function new(name, content, save,
- chunker, highlighter, printer)
+local function new(
+ name,
+ content,
+ save,
+ chunker,
+ highlighter,
+ printer,
+ truncer)
local _content, sel, ct, semantic
- local revmap = {}
local readonly = false
local lines = string.lines(content or '')
@@ -51,21 +57,10 @@ local function new(name, content, save,
--- @param chk function
local function luacontent(chk)
ct = 'lua'
- local ok, blocks, ast = chk(lines)
+ local ok, blocks = chk(lines)
if ok then
local len = #blocks
sel = len + 1
- local anaok, ana = pcall(analyzer.analyze, ast)
- if anaok then
- for bi, v in ipairs(blocks) do
- if (v.pos) then
- for _, l in ipairs(v.pos:enumerate()) do
- revmap[l] = bi
- end
- end
- end
- semantic = bsi.convert(ana, revmap)
- end
else
readonly = true
sel = 1
@@ -82,7 +77,7 @@ local function new(name, content, save,
plaintext()
end
- return {
+ local self = {
name = name or 'untitled',
content = _content,
content_type = ct,
@@ -90,13 +85,23 @@ local function new(name, content, save,
chunker = chunker,
highlighter = highlighter,
printer = printer,
+ truncer = truncer,
+ revmap = {},
semantic = semantic,
selection = sel,
readonly = readonly
}
+ local id = tostring(self):gsub('table: ', '')
+ self.id = id
+ return self
+end
+
+--- @param self BufferModel
+local function lateinit(self)
+ self:analyze()
end
---- @class BufferModel
+--- @class BufferModel : Object
--- @field name string
--- @field content Dequeue -- Content
--- @field content_type ContentType
@@ -105,17 +110,40 @@ end
--- @field loaded integer?
--- @field readonly boolean
--- @field semantic BufferSemanticInfo?
+--- @field revmap table?
---
--- @field chunker Chunker
--- @field highlighter Highlighter
--- @field printer Printer
+--- @field truncer function
--- @field move_selection function
--- @field get_selection function
--- @field get_selected_text function
--- @field delete_selected_text function
--- @field replace_selected_text function
--- @field get_text_content function
-BufferModel = class.create(new)
+BufferModel = class.create(new, lateinit)
+
+function BufferModel:get_id()
+ return self.id
+end
+
+function BufferModel:analyze()
+ if self.content_type ~= 'lua' then return end
+ local lines = string.lines(self:get_text_content())
+ local ok, blocks, ast = self.chunker(lines)
+ if not ok then return end
+ local anaok, ana = pcall(analyzer.analyze, ast)
+ if not anaok then return end
+ for bi, v in ipairs(blocks) do
+ if (v.pos) then
+ for _, l in ipairs(v.pos:enumerate()) do
+ self.revmap[l] = bi
+ end
+ end
+ end
+ self.semantic = bsi.convert(ana, self.revmap)
+end
function BufferModel:rechunk()
if self.content_type ~= 'lua' then return end
@@ -127,6 +155,7 @@ end
function BufferModel:save()
self:highlight()
local text = self:get_text_content()
+ self:analyze()
return self.save_file(text)
end
@@ -215,7 +244,7 @@ function BufferModel:set_selection(sel)
end
--- Get index of selected line/block
---- @return integer
+--- @return integer blocknum
function BufferModel:get_selection()
return self.selection
end
diff --git a/src/model/editor/bufferSemanticInfo.lua b/src/model/editor/bufferSemanticInfo.lua
index 76f7e500..4d854ec8 100644
--- a/src/model/editor/bufferSemanticInfo.lua
+++ b/src/model/editor/bufferSemanticInfo.lua
@@ -5,21 +5,30 @@ require('util.table')
--- @class Definition: Assignment
--- @field block blocknum
+--- @class RequireCall: Require
+--- @field block blocknum
+
--- @class BufferSemanticInfo
--- @field definitions Definition[]
+--- @field requires RequireCall[]
--- @param si SemanticInfo
--- @param rev table
--- @return BufferSemanticInfo
local function convert(si, rev)
- local as = si.assignments
- local defs = table.map(as, function(a)
+ local blockmap = function(a)
local r = table.clone(a)
r.block = rev[a.line]
return r
- end)
+ end
+ local as = si.assignments
+ local defs = table.map(as, blockmap)
+ local rs = si.requires
+ local reqs = table.map(rs, blockmap)
+
return {
definitions = defs,
+ requires = reqs,
}
end
diff --git a/src/model/editor/editorModel.lua b/src/model/editor/editorModel.lua
index 75bda4ba..c9fb4cbe 100644
--- a/src/model/editor/editorModel.lua
+++ b/src/model/editor/editorModel.lua
@@ -6,13 +6,13 @@ local class = require('util.class')
--- @class EditorModel
--- @field input UserInputModel
---- @field buffer BufferModel?
+--- @field buffers Dequeue
--- @field search Search
--- @field cfg Config
EditorModel = class.create(function(cfg)
return {
input = UserInputModel(cfg, LuaEval()),
- buffer = nil,
+ buffers = Dequeue.new({}, 'BufferModel'),
search = Search(cfg),
cfg = cfg,
}
diff --git a/src/model/input/history.lua b/src/model/input/history.lua
index 63d24585..d40e2284 100644
--- a/src/model/input/history.lua
+++ b/src/model/input/history.lua
@@ -106,5 +106,5 @@ function History:_dump(f)
end)
local i = self.index or '-'
local l = ' [' .. self:length() .. '] '
- log(i .. l .. Debug.text_table(t, false, nil, 64))
+ log(i .. l .. Debug.text_table(t, false, nil, 64) )
end
diff --git a/src/model/interpreter/eval/evaluator.lua b/src/model/interpreter/eval/evaluator.lua
index 12161efb..642870be 100644
--- a/src/model/interpreter/eval/evaluator.lua
+++ b/src/model/interpreter/eval/evaluator.lua
@@ -48,7 +48,7 @@ end
--- @param s string[]
--- @return boolean ok
--- @return str content|errpr
---- @return AST? ast
+--- @return luaAST? ast
local function default_apply(self, s)
local valid, errors = validate(self, s)
local parser = self.parser
diff --git a/src/model/lang/lua/analyze.lua b/src/model/lang/lua/analyze.lua
index f4c47ed7..1cd29ff7 100644
--- a/src/model/lang/lua/analyze.lua
+++ b/src/model/lang/lua/analyze.lua
@@ -1,19 +1,7 @@
require('util.tree')
+require('model.lang.lua.semantic_info')
---- @alias AssignmentType
---- | 'function'
---- | 'method'
---- | 'local'
---- | 'global'
---- | 'field'
-
---- @class Assignment
---- @field name string
---- @field line integer
---- @field type AssignmentType
-
---- @class SemanticInfo
---- @field assignments Assignment[]
+--- utils
local keywords_list = {
"and",
@@ -43,7 +31,17 @@ for _, kw in pairs(keywords_list) do
keywords[kw] = true
end
---- @param ast AST
+--- @param n luaAST
+--- @return number?
+local function get_line_number(n)
+ local li = n.lineinfo
+ local li_f = type(li) == 'table' and li.first
+ return type(li_f) == 'table' and li_f.line or nil
+end
+
+--- assignments
+
+--- @param ast luaAST
--- @return string?
local function get_idx_stack(ast)
--- @return string?
@@ -61,7 +59,7 @@ local function get_idx_stack(ast)
return go(ast)
end
---- @param node AST
+--- @param node luaAST
--- @return boolean
local function is_idx_stack(node)
local st = get_idx_stack(node)
@@ -77,20 +75,11 @@ local function is_ident(id)
return string["match"](id, "^[%a_][%w_]*$") and not keywords[id]
end
---- @param node AST
+--- @param node luaAST
--- @return table?
local function definition_extractor(node)
local deftags = { 'Local', 'Localrec', 'Set' }
- local function get_line_number(n)
- local li = n.lineinfo
- local li_f = type(li) == 'table' and li.first
- return type(li_f) == 'table' and li_f.line
- end
- local function get_lhs_name(n)
- return n[1]
- end
-
if type(node) == 'table' and node.tag then
local tag = node.tag
if table.is_member(deftags, tag) then
@@ -170,7 +159,7 @@ local function definition_extractor(node)
at = 'global'
end
for i, w in ipairs(lhs) do
- local n = get_lhs_name(w)
+ local n = w[1]
if is_local and not rhs[i] then
dec_only = true
end
@@ -202,9 +191,9 @@ local function defmatch(name)
end
end
---- @param ast AST
---- @return SemanticInfo
-local function analyze(ast)
+--- @param ast luaAST
+--- @return Assignment[]
+local function get_assignments(ast)
local sets = table.flatten(
Tree.preorder(ast, definition_extractor)
)
@@ -231,7 +220,52 @@ local function analyze(ast)
table.insert(candidates, v)
end
end
- return { assignments = assignments }
+ return assignments
+end
+
+--- requires
+
+--- @param node luaAST
+--- @return Require[]
+local function req_extractor(node)
+ local calltags = { 'Call' }
+ if type(node) == 'table' and node.tag then
+ local tag = node.tag
+ if table.is_member(calltags, tag) then
+ local lhs = node[1]
+ local rhs = node[2]
+
+ if tag == 'Call'
+ and lhs.tag == 'Id' and lhs[1] == 'require'
+ then
+ local val = rhs[1]
+ local li = get_line_number(rhs)
+ return { { line = li, name = val } }
+ end
+ end
+ end
+ return {}
+end
+
+--- @param ast luaAST
+--- @return Require[]
+local function get_requires(ast)
+ local candidates = table.flatten(
+ Tree.preorder(ast, req_extractor)
+ )
+ local reqs = {}
+ for _, v in ipairs(candidates or {}) do
+ table.insert(reqs, v)
+ end
+ return reqs
+end
+
+--- @param ast luaAST
+--- @return string[]
+local function analyze(ast)
+ local assignments = get_assignments(ast)
+ local reqs = get_requires(ast)
+ return SemanticInfo(assignments, reqs)
end
return {
diff --git a/src/model/lang/lua/parser.lua b/src/model/lang/lua/parser.lua
index b5c66baa..ad3cf448 100644
--- a/src/model/lang/lua/parser.lua
+++ b/src/model/lang/lua/parser.lua
@@ -5,7 +5,7 @@ require("util.debug")
require("util.string.string")
require("util.dequeue")
---- @class luaAST : token[]
+--- @class luaAST : token
--- @alias CPos 'first'|'last'
@@ -288,6 +288,7 @@ return function(lib)
local w = wrap or 80
local ok, r = parse(code)
if ok then
+ --- @diagnostic disable-next-line: param-type-mismatch
local src = ast_to_src(r, {}, w)
return string.lines(src)
end
@@ -419,11 +420,24 @@ return function(lib)
end
end
+ --- @param code str
+ --- @param n integer?
+ --- @return string[]?
+ local function trunc(code, n)
+ local lines = n or 1
+ if lines == 1 then
+ local txt = string.lines(code)
+ local line1 = txt[1]:sub(1, -1) .. '…'
+ return { line1 }
+ end
+ end
+
return {
parse = parse,
pprint = pprint,
highlighter = highlighter,
ast_to_src = ast_to_src,
chunker = chunker,
+ trunc = trunc,
}
end
diff --git a/src/model/lang/lua/semantic_info.lua b/src/model/lang/lua/semantic_info.lua
new file mode 100644
index 00000000..908c8d15
--- /dev/null
+++ b/src/model/lang/lua/semantic_info.lua
@@ -0,0 +1,27 @@
+--- @class SemanticInfoBase
+--- @field name string
+--- @field line integer
+
+--- @alias AssignmentType
+--- | 'function'
+--- | 'method'
+--- | 'local'
+--- | 'global'
+--- | 'field'
+
+--- @class Assignment : SemanticInfoBase
+--- @field type AssignmentType
+
+--- @class Require : SemanticInfoBase
+
+local class = require('util.class')
+
+--- @class SemanticInfo
+--- @field assignments Assignment[]
+--- @field requires Require[]
+SemanticInfo = class.create(function(asn, reqs)
+ return {
+ assignments = asn or {},
+ requires = reqs or {},
+ }
+end)
diff --git a/src/model/lang/md/parser.lua b/src/model/lang/md/parser.lua
index 6d724b93..d5fccf89 100644
--- a/src/model/lang/md/parser.lua
+++ b/src/model/lang/md/parser.lua
@@ -69,7 +69,7 @@ end
--- @param input str
--- @param skip_posinfo boolean?
---- @return AST -- djot AST, distinct from metalua
+--- @return djotAST
local function parse(input, skip_posinfo)
local text = string.unlines(input)
local posinfo = not (skip_posinfo == true)
diff --git a/src/types.lua b/src/types.lua
index 1a644972..e53f9f24 100644
--- a/src/types.lua
+++ b/src/types.lua
@@ -42,6 +42,7 @@
--- @class ViewConfig table
--- @field font love.Font
--- @field iconfont love.Font
+--- @field statusline_border integer
--- @field fh integer -- font height
--- @field fw integer -- font width
--- @field lh integer -- line height
@@ -57,6 +58,7 @@
--- @field debugwidth integer
--- @field drawableWidth number
--- @field drawableChars integer
+--- @field fold_lines integer
--- @field drawtest boolean
--- @field sizedebug boolean
@@ -132,11 +134,12 @@
--- @class LoveState table
--- @field testing boolean
---- @field has_removable boolean
+--- @field has_removable boolean?
--- @field user_input UserInput?
--- @field app_state AppState
--- @field prev_state AppState?
--- @field editor EditorState?
+--- @field suspend_msg string?
--- @class LoveDebug table
--- @field show_snapshot boolean
@@ -165,3 +168,17 @@
---
--- @field tokenize fun(str): table
--- @field syntax_hl fun(table): SyntaxColoring
+
+---@alias FPSC
+---| 'T_L"
+---| 'T_R"
+---| 'off'
+---| 'T_L_B"
+---| 'T_R_B"
+
+--- @class Profile
+--- @field report table
+--- @field frame integer
+--- @field n_frames integer
+--- @field n_rows integer
+--- @field fpsc FPSC
diff --git a/src/util/class.lua b/src/util/class.lua
index a33b2818..ac72966d 100644
--- a/src/util/class.lua
+++ b/src/util/class.lua
@@ -1,7 +1,12 @@
+--- @alias Id string
+--- @class Object
+--- @field id Id
+
return {
--- Simple factory, to spare boilerplate
--- @param constructor function?
- create = function(constructor)
+ --- @param lateinit function?
+ create = function(constructor, lateinit)
local ret = {}
ret.__index = ret
local function new(...)
@@ -16,8 +21,10 @@ return {
if type(cls.new) == "function" then
return cls.new(...)
else
- local instance = new(...)
- setmetatable(instance, cls)
+ local instance = setmetatable(new(...), cls)
+ if type(lateinit) == "function" then
+ lateinit(instance)
+ end
return instance
end
end,
diff --git a/src/util/debug.lua b/src/util/debug.lua
index 352af995..1209f057 100644
--- a/src/util/debug.lua
+++ b/src/util/debug.lua
@@ -122,6 +122,10 @@ local function terse_hash(t, level, prev_seen, jsonify)
return res
end
+local function nontable(t)
+ return '(not a table) ' .. tostring(t)
+end
+
--- @param a table?
--- @param skip integer?
local function terse_array(a, skip)
@@ -142,7 +146,7 @@ local function terse_array(a, skip)
return res
else
- return ''
+ return nontable(a)
end
end
@@ -154,7 +158,9 @@ end
--- @param style dumpstyle?
--- @return string
local function terse_ast(ast, skip_lineinfo, style)
- if type(ast) ~= 'table' then return '' end
+ if type(ast) ~= 'table' then
+ return nontable(ast)
+ end
local style = style or 'json5'
--- @param t table?
@@ -307,7 +313,7 @@ Debug = {
--- @param t string[]?
--- @param no_ln boolean?
--- @param skip integer?
- --- @param trunc boolean?
+ --- @param trunc number?
--- @return string
text_table = function(t, no_ln, skip, trunc)
local res = '\n'
@@ -516,7 +522,7 @@ Log = {
once = once,
fire_once = function()
- if not love.DEBUG then return end
+ if not love or not love.DEBUG then return end
love.debug.once = love.debug.once + 1
end,
--- @param color integer
diff --git a/src/util/dequeue.lua b/src/util/dequeue.lua
index 99955256..fb5c4917 100644
--- a/src/util/dequeue.lua
+++ b/src/util/dequeue.lua
@@ -172,7 +172,7 @@ function Dequeue:_checked(i, add, f)
return ok, err
end
---- Insert element at index
+--- Pop element at index
--- @param i integer
--- @return boolean
--- @return string|any err_or_result
diff --git a/src/util/lua.lua b/src/util/lua.lua
index d9fd65a0..de1572fa 100644
--- a/src/util/lua.lua
+++ b/src/util/lua.lua
@@ -31,6 +31,9 @@ local t = {
end
end,
codeload = codeload,
+ b2s = function(b)
+ return b and '#t' or '#f'
+ end,
}
for k, v in pairs(t) do
diff --git a/src/util/scrollableContent.lua b/src/util/scrollableContent.lua
index b2e59e95..a5d4b98e 100644
--- a/src/util/scrollableContent.lua
+++ b/src/util/scrollableContent.lua
@@ -2,7 +2,7 @@ require("util.wrapped_text")
require("util.scrollable")
require("util.range")
---- @class ScrollableContent
+--- @class ScrollableContent: WrappedText
--- @field range Range?
--- @field size integer
--- @field size_max integer
diff --git a/src/util/table.lua b/src/util/table.lua
index d3d91ecc..81772d6d 100644
--- a/src/util/table.lua
+++ b/src/util/table.lua
@@ -98,6 +98,36 @@ function table.protect(t, fields)
return t, orig
end
+--- Log set operations
+--- @param t table
+--- @param fields? table
+--- @return table prepared
+--- @return table original
+function table.logset(t, fields)
+ local orig = t
+ local proxy = {}
+
+ setmetatable(proxy, {
+ __newindex = function(_, k, v)
+ if not fields then
+ Log(string.format("%s = %s", k, v))
+ else
+ local fs = {}
+ for _, f in ipairs(fields) do
+ fs[f] = f
+ end
+ if fs[k] then
+ Log(string.format("%s = %s", k, v))
+ end
+ end
+ orig[k] = v
+ end,
+ })
+ -- getmetatable(proxy).__metatable = 'no-no'
+ t = proxy
+ return t, orig
+end
+
--- @diagnostic disable-next-line: duplicate-set-field
function table.pack(...)
--- @class t
@@ -281,6 +311,17 @@ function table.find_by(self, pred)
end
end
+--- Find first element that the predicate holds for
+--- @param self table[]
+--- @param pred function
+--- @return any?
+function table.find_by_v(self, pred)
+ if not self or not pred then return end
+ for _, v in pairs(self) do
+ if pred(v) then return v end
+ end
+end
+
--- Filter elements that satisfy the predicate
--- enumerates sequentially
--- @param self table[]
@@ -352,3 +393,27 @@ function table.map(self, f)
end
return ret
end
+
+--- Tabulate array values with index (returns new table)
+--- @param self table
+--- @param f function
+--- @return table
+function table.imap(self, f)
+ local ret = {}
+ for i, v in ipairs(self) do
+ ret[i] = f(v, i)
+ end
+ return ret
+end
+
+--- Create a table of `n` elements by running `f`
+--- @param n integer
+--- @param f function
+--- @return table
+function table.fill(n, f)
+ local ret = {}
+ for i = 1, n do
+ ret[i] = f()
+ end
+ return ret
+end
diff --git a/src/util/view.lua b/src/util/view.lua
index 385ab219..cd4fb48f 100644
--- a/src/util/view.lua
+++ b/src/util/view.lua
@@ -1,3 +1,5 @@
+if not love or not love.graphics then return end
+
require("util.string.string")
--- @param cfg ViewConfig
@@ -21,8 +23,8 @@ end
--- @param cfg ViewConfig
local write_line = function(l, str, y, breaks, cfg)
local dy = y - (-l + 1 + breaks) * cfg.fh
- G.setFont(cfg.font)
- G.print(str, 0, dy)
+ gfx.setFont(cfg.font)
+ gfx.print(str, 0, dy)
end
--- Write a token to output
@@ -34,17 +36,17 @@ end
--- @param selected boolean
local write_token = function(dy, dx, token,
color, bgcolor, selected)
- G.push('all')
+ gfx.push('all')
if selected then
- G.setColor(color)
+ gfx.setColor(color)
local back = string.rep('█', string.ulen(token))
- G.print(back, dx, dy)
- G.setColor(bgcolor)
+ gfx.print(back, dx, dy)
+ gfx.setColor(bgcolor)
else
- G.setColor(color)
+ gfx.setColor(color)
end
- G.print(token, dx, dy)
- G.pop()
+ gfx.print(token, dx, dy)
+ gfx.pop()
end
--- Hide elements for debugging
@@ -73,61 +75,61 @@ BlendMode = Alpha AlphaMode
local blendModes = {
{ -- 1
name = 'Alpha AlphaM',
- blend = function() G.setBlendMode('alpha', "alphamultiply") end
+ blend = function() gfx.setBlendMode('alpha', "alphamultiply") end
},
{ -- 2
name = 'Alpha PreM',
- blend = function() G.setBlendMode('alpha', "premultiplied") end
+ blend = function() gfx.setBlendMode('alpha', "premultiplied") end
},
-- add
{
name = 'Add AlphaM',
- blend = function() G.setBlendMode('add', "alphamultiply") end
+ blend = function() gfx.setBlendMode('add', "alphamultiply") end
},
{
name = 'Add PreM',
- blend = function() G.setBlendMode('add', "premultiplied") end
+ blend = function() gfx.setBlendMode('add', "premultiplied") end
},
-- subtract
{
name = 'Subtract AlphaM',
- blend = function() G.setBlendMode('subtract', "alphamultiply") end
+ blend = function() gfx.setBlendMode('subtract', "alphamultiply") end
},
{
name = 'Subtract PreM',
- blend = function() G.setBlendMode('subtract', "premultiplied") end
+ blend = function() gfx.setBlendMode('subtract', "premultiplied") end
},
-- replace
{
name = 'Replace AlphaM',
- blend = function() G.setBlendMode('replace', "alphamultiply") end
+ blend = function() gfx.setBlendMode('replace', "alphamultiply") end
},
{
name = 'Replace PreM',
- blend = function() G.setBlendMode('replace', "premultiplied") end
+ blend = function() gfx.setBlendMode('replace', "premultiplied") end
},
-- pre only
{
name = 'Multiply PreM',
- blend = function() G.setBlendMode('multiply', "premultiplied") end
+ blend = function() gfx.setBlendMode('multiply', "premultiplied") end
},
{
name = 'Darken PreM',
- blend = function() G.setBlendMode('darken', "premultiplied") end
+ blend = function() gfx.setBlendMode('darken', "premultiplied") end
},
{
name = 'Lighten PreM',
- blend = function() G.setBlendMode('lighten', "premultiplied") end
+ blend = function() gfx.setBlendMode('lighten', "premultiplied") end
},
-- screen
{
name = 'Screen AlphaM',
- blend = function() G.setBlendMode('screen', "alphamultiply") end
+ blend = function() gfx.setBlendMode('screen', "alphamultiply") end
},
{
name = 'Screen PreM',
- blend = function() G.setBlendMode('screen', "premultiplied") end
+ blend = function() gfx.setBlendMode('screen', "premultiplied") end
},
}
@@ -188,7 +190,7 @@ local function draw_hl_text(text, highlight, cfg, options)
end
end
end
- G.setColor(color)
+ gfx.setColor(color)
local dy = (t_l - 1) * fh
local dx = (c - 1) * fw
write_token(dy, dx, char, color, bg, false)
@@ -196,12 +198,22 @@ local function draw_hl_text(text, highlight, cfg, options)
end
end
+--- @param canvas love.Canvas?
+local function screenshot(canvas)
+ if not love.DEBUG then return end
+ local cv = canvas or gfx.getCanvas()
+ local img = cv:newImageData()
+ local filename = os.time() .. ".png"
+ img:encode("png", filename)
+end
+
ViewUtils = {
get_drawable_height = get_drawable_height,
write_line = write_line,
write_token = write_token,
draw_hl_text = draw_hl_text,
conditional_draw = conditional_draw,
+ screenshot = screenshot,
blendModes = blendModes,
}
diff --git a/src/util/wrapped_text.lua b/src/util/wrapped_text.lua
index 28b1aab1..e375eede 100644
--- a/src/util/wrapped_text.lua
+++ b/src/util/wrapped_text.lua
@@ -37,7 +37,7 @@ require("util.lua")
--- @field n_breaks integer
---
--- @field wrap function
---- @field get_text function
+--- @field get_text fun(self): Dequeue
--- @field get_line function
--- @field get_text_length function
WrappedText = class.create()
diff --git a/src/view/canvas/bgView.lua b/src/view/canvas/bgView.lua
index 512137d8..ec660a14 100644
--- a/src/view/canvas/bgView.lua
+++ b/src/view/canvas/bgView.lua
@@ -10,14 +10,14 @@ function BGView:draw(drawable_height)
local w = cfg.w
local fh = cfg.fh
- G.push('all')
+ gfx.push('all')
-- background in case input is not visible
- G.rectangle("fill",
+ gfx.rectangle("fill",
0,
drawable_height - 2,
w,
fh * 2 + 2
)
- G.pop()
+ gfx.pop()
end
diff --git a/src/view/canvas/canvasView.lua b/src/view/canvas/canvasView.lua
index 9cf794da..d12f5197 100644
--- a/src/view/canvas/canvasView.lua
+++ b/src/view/canvas/canvasView.lua
@@ -4,7 +4,7 @@ require("view.canvas.terminalView")
local class = require("util.class")
require("util.view")
-local G = love.graphics
+local gfx = love.graphics
--- @class CanvasView : ViewBase
--- @field bg BGView
@@ -26,60 +26,60 @@ function CanvasView:draw(
local cfg = self.cfg
local test = cfg.drawtest
- G.reset()
- G.push('all')
- G.setBlendMode('alpha', 'alphamultiply') -- default
+ gfx.reset()
+ gfx.push('all')
+ gfx.setBlendMode('alpha', 'alphamultiply') -- default
if ViewUtils.conditional_draw('show_snapshot') then
if snapshot then
- G.draw(snapshot)
+ gfx.draw(snapshot)
end
self.bg:draw(drawable_height)
end
if not test then
if ViewUtils.conditional_draw('show_terminal') then
- -- G.setBlendMode('multiply', "premultiplied")
+ -- gfx.setBlendMode('multiply', "premultiplied")
TerminalView.draw(terminal, term_canvas, snapshot)
end
if ViewUtils.conditional_draw('show_canvas') then
- G.draw(canvas)
+ gfx.draw(canvas)
end
- G.setBlendMode('alpha', 'alphamultiply') -- default
+ gfx.setBlendMode('alpha', 'alphamultiply') -- default
else
- G.setBlendMode('alpha', 'alphamultiply') -- default
+ gfx.setBlendMode('alpha', 'alphamultiply') -- default
for i = 0, love.test_grid_y - 1 do
for j = 0, love.test_grid_x - 1 do
local off_x = cfg.debugwidth * cfg.fw
local off_y = cfg.debugheight * cfg.fh
local dx = j * off_x
local dy = i * off_y
- G.reset()
- G.translate(dx, dy)
+ gfx.reset()
+ gfx.translate(dx, dy)
local index = (i * love.test_grid_x) + j + 1
local b = ViewUtils.blendModes[index]
if b then
- -- G.setBlendMode('alpha') -- default
+ -- gfx.setBlendMode('alpha') -- default
if ViewUtils.conditional_draw('show_terminal') then
b.blend()
TerminalView.draw(terminal, term_canvas, snapshot)
end
- G.setBlendMode('alpha') -- default
+ gfx.setBlendMode('alpha') -- default
if ViewUtils.conditional_draw('show_canvas') then
- G.draw(canvas)
+ gfx.draw(canvas)
end
- G.setBlendMode('alpha') -- default
- G.setColor(1, 1, 1, 1)
- G.setFont(cfg.labelfont)
+ gfx.setBlendMode('alpha') -- default
+ gfx.setColor(1, 1, 1, 1)
+ gfx.setFont(cfg.labelfont)
- -- G.print(index .. ' ' .. b.name)
- G.print(b.name)
+ -- gfx.print(index .. ' ' .. b.name)
+ gfx.print(b.name)
end
end
end
end
- G.pop()
+ gfx.pop()
end
diff --git a/src/view/canvas/terminalView.lua b/src/view/canvas/terminalView.lua
index 50c56bf6..12a44759 100644
--- a/src/view/canvas/terminalView.lua
+++ b/src/view/canvas/terminalView.lua
@@ -1,4 +1,4 @@
-local G = love.graphics
+local gfx = love.graphics
--- @class TerminalView
TerminalView = {}
@@ -10,11 +10,11 @@ local function terminal_draw(terminal, canvas, overlay)
terminal.char_width, terminal.char_height
-- if terminal.dirty or overlay then
- G.push('all')
+ gfx.push('all')
- G.setCanvas(canvas)
- G.setFont(terminal.font)
- G.clear(terminal.clear_color_alpha)
+ gfx.setCanvas(canvas)
+ gfx.setFont(terminal.font)
+ gfx.clear(terminal.clear_color_alpha)
local font_height = terminal.font:getHeight()
for y, row in ipairs(terminal.buffer) do
@@ -41,33 +41,33 @@ local function terminal_draw(terminal, canvas, overlay)
-- Character background
if not overlay then
- G.setColor(unpack(bg))
- G.rectangle("fill",
+ gfx.setColor(unpack(bg))
+ gfx.rectangle("fill",
left, top + (font_height - char_height),
char_width, char_height)
end
- local bm, am = G.getBlendMode()
- G.setBlendMode('alpha', "alphamultiply")
+ local bm, am = gfx.getBlendMode()
+ gfx.setBlendMode('alpha', "alphamultiply")
-- Character
- G.setColor(unpack(fg))
- G.print(char, left, top)
+ gfx.setColor(unpack(fg))
+ gfx.print(char, left, top)
- G.setBlendMode(bm, am)
+ gfx.setBlendMode(bm, am)
state.dirty = false
-- end
end
end
terminal.dirty = false
- G.pop()
+ gfx.pop()
-- end
if terminal.show_cursor then
- G.setFont(terminal.font)
+ gfx.setFont(terminal.font)
if love.timer.getTime() % 1 > 0.5 then
- G.print("_",
+ gfx.print("_",
(terminal.cursor_x - 1) * char_width,
(terminal.cursor_y - 1) * char_height)
end
@@ -76,15 +76,15 @@ end
--- @param terminal table
function TerminalView.draw(terminal, canvas, snapshot)
- G.setCanvas()
- G.push('all')
+ gfx.setCanvas()
+ gfx.push('all')
if snapshot then
terminal_draw(terminal, canvas, true)
else
terminal_draw(terminal, canvas)
end
- G.draw(canvas)
- G.setBlendMode('alpha') -- default
- G.pop()
+ gfx.draw(canvas)
+ gfx.setBlendMode('alpha') -- default
+ gfx.pop()
end
diff --git a/src/view/consoleView.lua b/src/view/consoleView.lua
index a46fa024..9c0d4f4e 100644
--- a/src/view/consoleView.lua
+++ b/src/view/consoleView.lua
@@ -8,12 +8,12 @@ require("util.color")
require("util.view")
require("util.debug")
-local G = love.graphics
+local gfx = love.graphics
--- @param cfg Config
--- @param ctrl ConsoleController
local function new(cfg, ctrl)
- return {
+ local self = {
title = TitleView,
canvas = CanvasView(cfg.view),
input = UserInputView(cfg.view, ctrl.input),
@@ -22,6 +22,9 @@ local function new(cfg, ctrl)
cfg = cfg,
drawable_height = ViewUtils.get_drawable_height(cfg.view),
}
+ --- hook the view in the controller
+ ctrl:init_view(self)
+ return self
end
--- @class ConsoleView
@@ -36,9 +39,8 @@ ConsoleView = class.create(new)
--- @param terminal table
--- @param canvas love.Canvas
---- @param input InputDTO
--- @param snapshot love.Image?
-function ConsoleView:draw(terminal, canvas, input, snapshot)
+function ConsoleView:draw(terminal, canvas, snapshot)
if love.DEBUG then
self:draw_placeholder()
end
@@ -50,11 +52,7 @@ function ConsoleView:draw(terminal, canvas, input, snapshot)
self.drawable_height, snapshot)
if ViewUtils.conditional_draw('show_input') then
- local time = nil
- if self.cfg.view.show_debug_timer then
- time = self.controller:get_timestamp()
- end
- self.input:draw(input, time)
+ self.input:draw()
end
end
@@ -73,15 +71,15 @@ function ConsoleView:draw_placeholder()
local band = self.cfg.view.fh
local w = self.cfg.view.w
local h = self.cfg.view.h
- G.push('all')
- G.setColor(Color[Color.yellow])
+ gfx.push('all')
+ gfx.setColor(Color[Color.yellow])
for o = -h, w, 2 * band do
- G.polygon("fill"
+ gfx.polygon("fill"
, o + 0, h
, o + h, 0
, o + h + band, 0
, o + band, h
)
end
- G.pop()
+ gfx.pop()
end
diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua
index b4a8aded..4f43eb64 100644
--- a/src/view/editor/bufferView.lua
+++ b/src/view/editor/bufferView.lua
@@ -15,12 +15,12 @@ local function new(cfg)
cfg = cfg,
LINES = l,
SCROLL_BY = math.floor(l / 2),
- w = cfg.drawableChars,
+ wrap_w = cfg.drawableChars,
content = nil,
content_type = nil,
more = { up = false, down = false },
- offset = 0,
+
buffer = nil
}
end
@@ -28,12 +28,12 @@ end
--- @class BufferView : ViewBase
--- @field content VisibleContent|VisibleStructuredContent
--- @field content_type ContentType
---- @field buffer BufferModel
+--- @field buffers Dequeue
---
+--- @field cfg ViewConfig
--- @field LINES integer
--- @field SCROLL_BY integer
---- @field w integer
---- @field offset integer
+--- @field wrap_w integer
--- @field more More
---
--- @field open function
@@ -53,32 +53,31 @@ function BufferView:open(buffer)
if not self.buffer then
error('no buffer')
end
- local cont = buffer.content_type
- self.content_type = cont
+ local ct = buffer.content_type
+ self.content_type = ct
- if cont == 'plain' or cont == 'md' then
+ if ct == 'plain' or ct == 'md' then
local bufcon = buffer:get_text_content()
self.buffer:highlight()
self.content = VisibleContent(
- self.w, bufcon, self.SCROLL_BY, L)
+ self.wrap_w, bufcon, self.SCROLL_BY, L)
self.hl = self.buffer:get_highlight()
- elseif cont == 'lua' then
+ elseif ct == 'lua' then
local bufcon = buffer:get_content()
self.content =
- VisibleStructuredContent(
- self.w,
+ VisibleStructuredContent({
+ wrap_w = self.wrap_w,
+ overscroll_max = self.SCROLL_BY,
+ size_max = L,
+ cfg = self.cfg,
+ },
bufcon,
- buffer.highlighter,
- self.SCROLL_BY,
- L)
+ buffer.highlighter)
else
error 'unknown filetype'
end
- -- TODO clean this up
- local clen = self.content:get_text_length()
- self.offset = math.max(clen - L, 0)
- local off = self.offset
+ local off = self.content.offset
if off > 0 then
self.more.up = true
end
@@ -118,7 +117,7 @@ function BufferView:get_state()
return {
filename = buf.name,
selection = buf.selection,
- offset = self.offset,
+ offset = self.content.offset,
}
end
@@ -139,7 +138,7 @@ function BufferView:refresh(moved)
local sel = self.buffer:get_selection()
if self.content_type == 'lua' then
local vsc = self.content
- local blocks = vsc.blocks
+ local blocks = vsc.v_blocks
blocks:move(moved, sel)
vsc:recalc_range()
else
@@ -150,7 +149,7 @@ function BufferView:refresh(moved)
end
local clen = self.content:get_content_length()
- local off = self.offset
+ local off = self.content.offset
local si = 1 + off
local ei = math.min(self.LINES, clen + 1) + off
self:_update_visible(Range(si, ei))
@@ -160,6 +159,11 @@ end
--- scrolling ---
-------------------
+--- @return integer
+function BufferView:get_offset()
+ return self.content.offset
+end
+
--- @private
--- @return Range
function BufferView:_get_end_range()
@@ -190,8 +194,7 @@ function BufferView:scroll(dir, by, warp)
end
end
end)()
- local o = self.content:move_range(n)
- self.offset = self.offset + o
+ self.content:move_range(n)
end
--- @param off integer
@@ -253,9 +256,13 @@ function BufferView:follow_selection()
end
end
+--------------
+--- draw ---
+--------------
+
--- @param special boolean
function BufferView:draw(special)
- local G = love.graphics
+ local gfx = love.graphics
local cf_colors = self.cfg.colors
local colors = cf_colors.editor
local font = self.cfg.font
@@ -265,17 +272,17 @@ function BufferView:draw(special)
--- @type VisibleContent|VisibleStructuredContent
local content_text = vc:get_visible()
local last_line_n = #content_text
- local width, height = G.getDimensions()
+ local width, height = gfx.getDimensions()
local draw_background = function()
- G.push('all')
- G.setColor(colors.bg)
- G.rectangle("fill", 0, 0, width, height)
- G.setColor(Color.with_alpha(colors.fg, .0625))
+ gfx.push('all')
+ gfx.setColor(colors.bg)
+ gfx.rectangle("fill", 0, 0, width, height)
+ gfx.setColor(Color.with_alpha(colors.fg, .0625))
local bh = math.min(last_line_n, self.cfg.lines) * fh
- G.rectangle("fill", 0, 0, width, bh)
- G.pop()
+ gfx.rectangle("fill", 0, 0, width, bh)
+ gfx.pop()
end
local draw_highlight = function()
@@ -284,19 +291,19 @@ function BufferView:draw(special)
local highlight_line = function(ln)
if not ln then return end
if special then
- G.setColor(colors.highlight_special)
+ gfx.setColor(colors.highlight_special)
else
if ls then
- G.setColor(colors.highlight_loaded)
+ gfx.setColor(colors.highlight_loaded)
else
- G.setColor(colors.highlight)
+ gfx.setColor(colors.highlight)
end
end
local l_y = (ln - 1) * fh
- G.rectangle('fill', 0, l_y, width, fh)
+ gfx.rectangle('fill', 0, l_y, width, fh)
end
- local off = self.offset
+ local off = self.content.offset
for _, w in ipairs(ws) do
for _, v in ipairs(w) do
if self.content.range:inc(v) then
@@ -313,7 +320,7 @@ function BufferView:draw(special)
end
local draw_text = function()
- G.setFont(font)
+ gfx.setFont(font)
if self.content_type == 'lua' then
local vbl = vc:get_visible_blocks()
for _, block in ipairs(vbl) do
@@ -323,7 +330,7 @@ function BufferView:draw(special)
local text = wt:get_text()
local highlight = { hl = block.highlight }
local ltf = function(l)
- return l + rs - 1 - self.offset
+ return l + rs - 1 - self.content.offset
end
local ctf = function(a) return a end
local limit = self.cfg.lines
@@ -337,9 +344,9 @@ function BufferView:draw(special)
if love.DEBUG then
--- phantom text
- G.setColor(Color.with_alpha(colors.fg, 0.3))
+ gfx.setColor(Color.with_alpha(colors.fg, 0.3))
local text = string.unlines(content_text)
- G.print(text)
+ gfx.print(text)
end
elseif self.content_type == 'md' then
local text = vc:get_visible()
@@ -357,10 +364,10 @@ function BufferView:draw(special)
ltf = ltf, ctf = ctf, limit = limit,
})
elseif self.content_type == 'plain' then
- G.setColor(colors.fg)
+ gfx.setColor(colors.fg)
local text = string.unlines(content_text)
- G.print(text)
+ gfx.print(text)
end
end
@@ -370,24 +377,24 @@ function BufferView:draw(special)
local lnc = colors.fg
local x = self.cfg.w - font:getWidth(' ') - 3
local lnvc = Color.with_alpha(lnc, 0.2)
- G.setColor(lnvc)
- G.rectangle("fill", x, 0, 2, self.cfg.h)
+ gfx.setColor(lnvc)
+ gfx.rectangle("fill", x, 0, 2, self.cfg.h)
local seen = {}
for ln = 1, self.LINES do
local l_y = (ln - 1) * fh
- local vln = ln + self.offset
+ local vln = ln + self.content.offset
local ln_w = self.content.wrap_reverse[vln]
if ln_w then
local l = string.format('%3d', ln_w)
local l_x = self.cfg.w - font:getWidth(l)
local l_xv = l_x - font:getWidth(l) - 3.5
if showap then
- G.setColor(lnvc)
- G.print(string.format('%3d', vln), l_xv, l_y)
+ gfx.setColor(lnvc)
+ gfx.print(string.format('%3d', vln), l_xv, l_y)
end
if not seen[ln_w] then
- G.setColor(lnc)
- G.print(l, l_x, l_y)
+ gfx.setColor(lnc)
+ gfx.print(l, l_x, l_y)
seen[ln_w] = true
end
end
diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua
index e86b68e0..a9d1cba0 100644
--- a/src/view/editor/editorView.lua
+++ b/src/view/editor/editorView.lua
@@ -12,18 +12,18 @@ local function new(cfg, ctrl)
cfg = cfg,
controller = ctrl,
input = UserInputView(cfg, ctrl.input),
- buffer = BufferView(cfg),
+ buffers = {},
search = SearchView(cfg, ctrl.search),
}
--- hook the view in the controller
- ctrl.view = ev
+ ctrl:init_view(ev)
return ev
end
--- @class EditorView : ViewBase
--- @field controller EditorController
--- @field input UserInputView
---- @field buffer BufferView
+--- @field buffers { [string]: BufferView }
--- @field search SearchView
EditorView = class.create(new)
@@ -31,18 +31,57 @@ function EditorView:draw()
local ctrl = self.controller
local mode = ctrl:get_mode()
if mode == 'search' then
- self.search:draw(ctrl.search:get_input())
+ self.search:draw()
else
local spec = mode == 'reorder'
- self.buffer:draw(spec)
+ local bv = self:get_current_buffer()
+
+ if ViewUtils.conditional_draw('show_buffer') then
+ bv:draw(spec)
+ end
if ViewUtils.conditional_draw('show_input') then
- local input = ctrl:get_input()
- self.input:draw(input)
+ self.input:draw()
end
end
end
+--- @param buffer BufferModel
+--- @return BufferView
+function EditorView:open(buffer)
+ local bid = buffer:get_id()
+ local opn = self.buffers[bid]
+ if not opn then
+ local v = BufferView(self.cfg)
+ self.buffers[bid] = v
+ v:open(buffer)
+ return v
+ end
+ return opn
+end
+
+--- @return BufferView
+function EditorView:get_current_buffer()
+ local ctrl = self.controller
+ local bm = ctrl:get_active_buffer()
+ local bid = bm:get_id()
+ return self.buffers[bid]
+end
+
+--- @param bid string
+--- @return BufferView
+function EditorView:get_buffer(bid)
+ return self.buffers[bid]
+end
+
--- @param moved integer?
function EditorView:refresh(moved)
- self.buffer:refresh(moved)
+ self:get_current_buffer():refresh(moved)
+ self:update_input()
+end
+
+function EditorView:update_input()
+ local ctrl = self.controller
+ local input = ctrl:get_input()
+ local status = ctrl.input:get_status()
+ self.input:render(input, status)
end
diff --git a/src/view/editor/search/resultsView.lua b/src/view/editor/search/resultsView.lua
index 880e8ee1..c24015f5 100644
--- a/src/view/editor/search/resultsView.lua
+++ b/src/view/editor/search/resultsView.lua
@@ -16,14 +16,14 @@ ResultsView = class.create(new)
function ResultsView:draw(results)
local colors = self.cfg.colors.editor
local fh = self.cfg.fh * 1.032 -- magic constant
- local width, height = G.getDimensions()
+ local width, height = gfx.getDimensions()
local has_results = (results.results and #(results.results) > 0)
local draw_background = function()
- G.push('all')
- G.setColor(colors.results.bg)
- G.rectangle("fill", 0, 0, width, height)
- G.pop()
+ gfx.push('all')
+ gfx.setColor(colors.results.bg)
+ gfx.rectangle("fill", 0, 0, width, height)
+ gfx.pop()
end
local draw_results = function()
@@ -40,33 +40,33 @@ function ResultsView:draw(results)
return ""
end
end
- G.push('all')
- G.setFont(self.cfg.font)
+ gfx.push('all')
+ gfx.setFont(self.cfg.font)
if not has_results then
- G.setColor(Color.with_alpha(colors.results.fg, 0.5))
- G.print("No results", 25, 0)
+ gfx.setColor(Color.with_alpha(colors.results.fg, 0.5))
+ gfx.print("No results", 25, 0)
else
for i, v in ipairs(results.results) do
local ln = i
local lh = (ln - 1) * fh
local t = v.r.type
local label = getLabel(t)
- G.setColor(Color.with_alpha(colors.results.fg, 0.5))
- G.print(label, 2, lh + 2)
- G.setColor(colors.results.fg)
- G.print(v.r.name, 25, lh)
+ gfx.setColor(Color.with_alpha(colors.results.fg, 0.5))
+ gfx.print(label, 2, lh + 2)
+ gfx.setColor(colors.results.fg)
+ gfx.print(v.r.name, 25, lh)
end
end
- G.pop()
+ gfx.pop()
end
local draw_selection = function()
local highlight_line = function(ln)
if not ln then return end
- G.setColor(colors.highlight)
+ gfx.setColor(colors.highlight)
local l_y = (ln - 1) * fh
- G.rectangle('fill', 0, l_y, width, fh)
+ gfx.rectangle('fill', 0, l_y, width, fh)
end
local v = results.selection
highlight_line(v)
diff --git a/src/view/editor/search/searchView.lua b/src/view/editor/search/searchView.lua
index 6e8fe433..ed8cb704 100644
--- a/src/view/editor/search/searchView.lua
+++ b/src/view/editor/search/searchView.lua
@@ -5,11 +5,13 @@ require("view.input.userInputView")
--- @param cfg ViewConfig
--- @param ctrl SearchController
local function new(cfg, ctrl)
- return {
+ local self = {
controller = ctrl,
results = ResultsView(cfg),
input = UserInputView(cfg, ctrl.input)
}
+ ctrl:init_view(self)
+ return self
end
--- @class SearchView
@@ -18,12 +20,13 @@ end
--- @field input UserInputView
SearchView = class.create(new)
---- @param input InputDTO
-function SearchView:draw(input)
+function SearchView:draw()
local ctrl = self.controller
local rs = ctrl:get_results()
+ gfx.push("all")
self.results:draw(rs)
if ViewUtils.conditional_draw('show_input') then
- self.input:draw(input)
+ self.input:draw()
end
+ gfx.pop()
end
diff --git a/src/view/editor/visibleContent.lua b/src/view/editor/visibleContent.lua
index 68223f78..9ab4d7f6 100644
--- a/src/view/editor/visibleContent.lua
+++ b/src/view/editor/visibleContent.lua
@@ -134,6 +134,7 @@ function VisibleContent:move_range(by)
if r then
local nr, n = r:translate_limit(by, 1, upper)
self:set_range(nr)
+ self.offset = nr.start - 1
return n
end
end
diff --git a/src/view/editor/visibleStructuredContent.lua b/src/view/editor/visibleStructuredContent.lua
index 4c63c50d..d0cddb37 100644
--- a/src/view/editor/visibleStructuredContent.lua
+++ b/src/view/editor/visibleStructuredContent.lua
@@ -1,16 +1,24 @@
require("view.editor.visibleBlock")
require("util.wrapped_text")
+require("util.scrollable")
require("util.range")
+--- @class VSCOpts
+--- @field wrap_w integer
+--- @field size_max integer
+--- @field overscroll_max integer
+--- @field cfg ViewConfig
--- @alias ReverseMap Dequeue
--- Inverse mapping from line number to block index
--- @class VisibleStructuredContent: WrappedText
---- @field overscroll_max integer
+--- @field offset integer
+--- @field overscroll integer
+--- @field highlighter fun(c: string[]): SyntaxColoring
--- @field size_max integer
--- @field range Range?
---- @field blocks Dequeue
+--- @field v_blocks Dequeue
--- @field reverse_map ReverseMap
---
--- @field set_range fun(self, Range)
@@ -36,19 +44,19 @@ setmetatable(VisibleStructuredContent, {
end,
})
---- @param w integer
+--- @param opts VSCOpts
--- @param blocks Block[]
--- @param highlighter fun(c: string[]): SyntaxColoring
---- @param overscroll integer
---- @param size_max integer
--- @return VisibleStructuredContent
-function VisibleStructuredContent.new(w, blocks, highlighter,
- overscroll, size_max)
+function VisibleStructuredContent.new(
+ opts,
+ blocks,
+ highlighter)
local self = setmetatable({
+ overscroll = opts.overscroll_max,
+ opts = opts,
highlighter = highlighter,
- size_max = size_max,
- overscroll_max = overscroll,
- w = w,
+ offset = 0,
}, VisibleStructuredContent)
self:load_blocks(blocks)
self:to_end()
@@ -59,7 +67,7 @@ end
--- Set the visible range so that last of the content is visible
function VisibleStructuredContent:to_end()
self.range = Scrollable.to_end(
- self.size_max, self:get_text_length())
+ self.opts.size_max, self:get_text_length())
self.offset = self.range.start - 1
end
@@ -70,16 +78,17 @@ function VisibleStructuredContent:load_blocks(blocks)
local revmap = Dequeue.typed('integer')
local visible_blocks = Dequeue()
local off = 0
+ local w = self.opts.wrap_w
for bi, v in ipairs(blocks) do
if v:is_empty() then
fulltext:append('')
local npos = v.pos:translate(off)
visible_blocks:append(
- VisibleBlock(self.w, { '' }, {}, v.pos, npos))
+ VisibleBlock(w, { '' }, {}, v.pos, npos))
else
fulltext:append_all(v.lines)
local hl = self.highlighter(v.lines)
- local vblock = VisibleBlock(self.w, v.lines, hl,
+ local vblock = VisibleBlock(w, v.lines, hl,
v.pos, v.pos:translate(off))
off = off + vblock.wrapped.n_breaks
visible_blocks:append(vblock)
@@ -90,15 +99,15 @@ function VisibleStructuredContent:load_blocks(blocks)
end
end
end
- WrappedText._init(self, self.w, fulltext)
+ WrappedText._init(self, self.opts.wrap_w, fulltext)
self:_init()
self.reverse_map = revmap
- self.blocks = visible_blocks
+ self.v_blocks = visible_blocks
end
function VisibleStructuredContent:recalc_range()
local ln, aln = 1, 1
- for _, v in ipairs(self.blocks) do
+ for _, v in ipairs(self.v_blocks) do
local l = #(v.wrapped.orig)
local al = #(v.wrapped.text)
v.pos = Range(ln, ln + l - 1)
@@ -121,7 +130,7 @@ end
--- @protected
function VisibleStructuredContent:_update_overscroll()
local len = WrappedText.get_text_length(self)
- local over = math.min(self.overscroll_max, len)
+ local over = math.min(self.opts.overscroll_max, len)
self.overscroll = over
end
@@ -154,6 +163,7 @@ function VisibleStructuredContent:move_range(by)
local upper = self:get_text_length() + self.overscroll
local nr, n = r:translate_limit(by, 1, upper)
self:set_range(nr)
+ self.offset = nr.start - 1
return n
end
return 0
@@ -168,7 +178,7 @@ function VisibleStructuredContent:get_visible_blocks()
local si = self.wrap_reverse[self.range.start]
local ei = self.wrap_reverse[self.range.fin]
local sbi, sei = self.reverse_map[si], self.reverse_map[ei]
- return table.slice(self.blocks, sbi, sei)
+ return table.slice(self.v_blocks, sbi, sei)
end
--- @return integer
@@ -179,22 +189,22 @@ end
--- @param bn integer
--- @return Range?
function VisibleStructuredContent:get_block_pos(bn)
- local cl = #(self.blocks)
+ local cl = #(self.v_blocks)
if bn > 0 and bn <= cl then
- return self.blocks[bn].pos
+ return self.v_blocks[bn].pos
elseif cl == 0 then --- empty/new file
Range.singleton(1)
elseif bn == cl + 1 then
- return Range.singleton(self.blocks[cl].pos.fin + 1)
+ return Range.singleton(self.v_blocks[cl].pos.fin + 1)
end
end
--- @param bn integer
--- @return Range?
function VisibleStructuredContent:get_block_app_pos(bn)
- local cl = #(self.blocks)
+ local cl = #(self.v_blocks)
if bn > 0 and bn <= cl then
- return self.blocks[bn].app_pos
+ return self.v_blocks[bn].app_pos
elseif bn == cl + 1 then
local wr = self.wrap_reverse
return Range.singleton(#wr)
diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua
index ad2f5fa9..46742572 100644
--- a/src/view/input/statusline.lua
+++ b/src/view/input/statusline.lua
@@ -7,37 +7,39 @@ end)
--- @param status Status
---- @param nLines integer
---- @param time number?
-function Statusline:draw(status, nLines, time)
- local G = love.graphics
+--- @param start_y integer?
+function Statusline:draw(status, start_y)
+ local gfx = love.graphics
local cf = self.cfg
local colors = (function()
- if love.state.app_state == 'inspect' then
+ local state = love.state.app_state
+ if state == 'inspect' then
return cf.colors.statusline.inspect
- elseif love.state.app_state == 'running' then
+ elseif state == 'running' then
return cf.colors.statusline.user
- elseif love.state.app_state == 'editor' then
+ elseif state == 'editor' then
return cf.colors.statusline.editor
else
return cf.colors.statusline.console
end
end)()
- local h = cf.h
+
+ local h = start_y or 0
local w = cf.w
local fh = cf.fh
local font = cf.font
+ local sb = cf.statusline_border
+ local corr = sb / 2
- local sy = h - (1 + nLines) * fh
- local start_box = { x = 0, y = sy }
+ local start_box = { x = 0, y = h }
local endTextX = start_box.x + w - fh
local midX = (start_box.x + w) / 2
local function drawBackground()
- G.setColor(colors.bg)
- G.setFont(font)
- local corr = 2 -- correct for fractional slit left under the terminal
- G.rectangle("fill", start_box.x, start_box.y - corr, w, fh + corr)
+ gfx.setColor(colors.bg)
+ gfx.setFont(font)
+ gfx.rectangle("fill",
+ start_box.x, start_box.y - corr, w, fh + sb)
end
--- @param m More?
@@ -57,27 +59,29 @@ function Statusline:draw(status, nLines, time)
end
local function drawStatus()
+ local state = love.state.app_state
local custom = status.custom
local start_text = {
x = start_box.x + fh,
- y = start_box.y - 2,
+ y = start_box.y,
}
- G.setColor(colors.fg)
+ gfx.setColor(colors.fg)
local label = status.label
if label then
- G.print(label, start_text.x, start_text.y)
+ gfx.print(label, start_text.x, start_text.y)
end
if love.DEBUG then
- G.setColor(cf.colors.debug)
+ gfx.setColor(cf.colors.debug)
if love.state.testing then
- G.print('testing', midX - (8 * cf.fw), start_text.y)
- end
- G.print(love.state.app_state, midX - (13 * cf.fw), start_text.y)
- if time then
- G.print(tostring(time), midX, start_text.y)
+ gfx.print('testing',
+ midX - (8 * cf.fw),
+ start_text.y + corr)
end
- G.setColor(colors.fg)
+ local lw = font:getWidth(state) / 2
+ gfx.print((state or '???'),
+ midX - lw, start_text.y)
+ gfx.setColor(colors.fg)
end
local c = status.cursor
@@ -98,50 +102,50 @@ function Statusline:draw(status, nLines, time)
local more_b = morelabel(custom.buffer_more) .. ' '
local more_i = morelabel(status.input_more) .. ' '
- G.setColor(colors.fg)
- local w_il = G.getFont():getWidth(" 999:9999")
- local w_br = G.getFont():getWidth("B999 L999-999(99)")
- local w_mb = G.getFont():getWidth(" ↕↕ ")
- local w_mi = G.getFont():getWidth(" ↕↕ ")
+ gfx.setColor(colors.fg)
+ local w_il = gfx.getFont():getWidth(" 999:9999")
+ local w_br = gfx.getFont():getWidth("B999 L999-999(99)")
+ local w_mb = gfx.getFont():getWidth(" ↕↕ ")
+ local w_mi = gfx.getFont():getWidth(" ↕↕ ")
local s_mb = endTextX - w_br - w_il - w_mi - w_mb
- local cw_p = G.getFont():getWidth(t_blp)
- local cw_il = G.getFont():getWidth(t_ic)
+ local cw_p = gfx.getFont():getWidth(t_blp)
+ local cw_il = gfx.getFont():getWidth(t_ic)
local sxl = endTextX - (cw_p + w_il + w_mi)
local s_mi = endTextX - w_il
- G.setFont(self.cfg.font)
- G.setColor(colors.fg)
- if colors.fg2 then G.setColor(colors.fg2) end
+ gfx.setFont(self.cfg.font)
+ gfx.setColor(colors.fg)
+ if colors.fg2 then gfx.setColor(colors.fg2) end
--- cursor pos
- G.print(t_ic, endTextX - cw_il, start_text.y)
+ gfx.print(t_ic, endTextX - cw_il, start_text.y)
--- input more
- G.print(more_i, s_mi, start_text.y - 3)
+ gfx.print(more_i, s_mi, start_text.y - 3)
- G.setColor(colors.fg)
+ gfx.setColor(colors.fg)
if custom.mode == 'reorder'
and custom.content_type == 'plain' then
- G.setColor(colors.special)
+ gfx.setColor(colors.special)
end
--- block line range / line
- G.print(t_blp, sxl, start_text.y)
- G.setColor(colors.fg)
+ gfx.print(t_blp, sxl, start_text.y)
+ gfx.setColor(colors.fg)
--- block number
if custom.content_type == 'lua' then
- local bpw = G.getFont():getWidth(t_bbp)
+ local bpw = gfx.getFont():getWidth(t_bbp)
local sxb = sxl - bpw
if sel == lim then
- G.setColor(colors.indicator)
+ gfx.setColor(colors.indicator)
end
if custom.mode == 'reorder' then
- G.setColor(colors.special)
+ gfx.setColor(colors.special)
end
- G.print(t_bbp, sxb, start_text.y)
+ gfx.print(t_bbp, sxb, start_text.y)
end
--- buffer more
- G.setColor(colors.fg)
- G.print(more_b, s_mb, start_text.y)
+ gfx.setColor(colors.fg)
+ gfx.print(more_b, s_mb, start_text.y)
else
--- normal statusline
local pos_c = ':' .. c.c
@@ -154,22 +158,22 @@ function Statusline:draw(status, nLines, time)
l_lim = status.n_lines
end
if ln == l_lim then
- G.setColor(colors.indicator)
+ gfx.setColor(colors.indicator)
end
local pos_l = 'L' .. ln
- local lw = G.getFont():getWidth(pos_l)
- local cw = G.getFont():getWidth(pos_c)
+ local lw = gfx.getFont():getWidth(pos_l)
+ local cw = gfx.getFont():getWidth(pos_c)
local sx = endTextX - (lw + cw)
- G.print(pos_l, sx, start_text.y)
- G.setColor(colors.fg)
- G.print(pos_c, sx + lw, start_text.y)
+ gfx.print(pos_l, sx, start_text.y)
+ gfx.setColor(colors.fg)
+ gfx.print(pos_c, sx + lw, start_text.y)
end
end
end
- G.push('all')
+ gfx.push('all')
drawBackground()
drawStatus()
- G.pop()
+ gfx.pop()
end
diff --git a/src/view/input/userInputView.lua b/src/view/input/userInputView.lua
index ad63190d..5617a225 100644
--- a/src/view/input/userInputView.lua
+++ b/src/view/input/userInputView.lua
@@ -8,11 +8,17 @@ require("util.view")
--- @param cfg ViewConfig
--- @param ctrl UserInputController
local new = function(cfg, ctrl)
+ local gfx = love.graphics
+ local w = gfx.getWidth()
+ --- max lines + statusline
+ local h = cfg.input_max * cfg.fh + cfg.fh
return {
cfg = cfg,
controller = ctrl,
statusline = Statusline(cfg),
oneshot = ctrl.model.oneshot,
+ start_h = h,
+ canvas = gfx.newCanvas(w, h),
}
end
@@ -20,42 +26,67 @@ end
--- @field controller UserInputController
--- @field statusline table
--- @field oneshot boolean
+--- @field canvas love.Canvas
UserInputView = class.create(new)
---- @param input InputDTO
---- @param time number?
-function UserInputView:draw_input(input, time)
- local G = love.graphics
+local get_colors = function(cf_colors)
+ if love.state.app_state == 'inspect' then
+ return cf_colors.input.inspect
+ elseif love.state.app_state == 'running' then
+ return cf_colors.input.user
+ else
+ return cf_colors.input.console
+ end
+end
+
+--- The Overflow
+--- When the cursor is on the last character of the line
+--- we display it at the start of the next line as that's
+--- where newly added text will appear
+--- However, when the line also happens to be wrap-length,
+--- there either is no next line yet, or it would look the
+--- same as if it was at the start of the next.
+--- Hence, the overflow phantom line.
+local calc_overflow = function(w, text, cursor)
+ local cl, cc = cursor.l, cursor.c
+ local acc = cc - 1
+ --- overflow binary and actual height (in lines)
+ local overflow = 0
+ local of_h = 0
+ local curline = text[cl]
+ local clen = string.ulen(curline)
+ local q, rem = math.modf(acc / w)
+ local ofpos = rem == 0 and acc == clen and clen > 0
+ if ofpos
+ and string.is_non_empty_string_array(text)
+ then
+ overflow = 1
+ of_h = q
+ end
+ return overflow, of_h, ofpos
+end
+--- @param input InputDTO
+--- @param status Status
+function UserInputView:render_input(input, status)
+ local gfx = love.graphics
local cfg = self.cfg
- local status = self.controller:get_status()
local cf_colors = cfg.colors
- local colors = (function()
- if love.state.app_state == 'inspect' then
- return cf_colors.input.inspect
- elseif love.state.app_state == 'running' then
- return cf_colors.input.user
- else
- return cf_colors.input.console
- end
- end)()
+ local colors = get_colors(cf_colors)
local fh = cfg.fh
local fw = cfg.fw
- local h = cfg.h
+ local h = 0
local drawableWidth = cfg.drawableWidth
local w = cfg.drawableChars
-- drawtest hack
- if drawableWidth < G.getWidth() / 3 then
+ if drawableWidth < gfx.getWidth() / 3 then
w = w * 2
end
local cursorInfo = self.controller:get_cursor_info()
local cl, cc = cursorInfo.cursor.l, cursorInfo.cursor.c
local acc = cc - 1
- --- overflow binary and actual height (in lines)
- local overflow = 0
- local of_h = 0
local text = input.text
local vc = input.visible
@@ -63,34 +94,18 @@ function UserInputView:draw_input(input, time)
vc:get_content_length(),
cfg.input_max)
- --- The Overflow
- --- When the cursor is on the last character of the line
- --- we display it at the start of the next line as that's
- --- where newly added text will appear
- --- However, when the line also happens to be wrap-length,
- --- there either is no next line yet, or it would look the
- --- same as if it was at the start of the next.
- --- Hence, the overflow phantom line.
- local curline = text[cl]
- local clen = string.ulen(curline)
- local q, rem = math.modf(acc / w)
- local ofpos = rem == 0 and acc == clen and clen > 0
- if ofpos
- and string.is_non_empty_string_array(text)
- then
- overflow = 1
- of_h = q
- end
+ local overflow, of_h, ofpos = calc_overflow(
+ w, text, cursorInfo.cursor)
- local apparentLines = inLines + overflow
- local inHeight = inLines * fh
+ local inHeight = (inLines + overflow) * fh
local apparentHeight = inHeight
- local y = h - (#text * fh)
local wrap_forward = vc.wrap_forward
local wrap_reverse = vc.wrap_reverse
- local start_y = h - apparentLines * fh
+
+ local vpH = gfx.getHeight()
+ self.start_h = vpH - (inLines + overflow + 1) * fh
local function drawCursor()
local y_offset = math.floor(acc / w)
@@ -100,202 +115,175 @@ function UserInputView:draw_input(input, time)
if vcl < 1 then return end
- local ch = start_y + (vcl - 1) * fh
+ local ch = vcl * fh
local x_offset = math.fmod(acc, w)
+ local x = (x_offset - .5) * fw
- G.push('all')
- G.setColor(cf_colors.input.cursor)
- G.print('|', (x_offset - .5) * fw, ch)
- G.pop()
+ gfx.push('all')
+ gfx.setColor(cf_colors.input.cursor)
+ gfx.print('|', x, ch)
+ gfx.pop()
end
local drawBackground = function()
- G.setColor(colors.bg)
- G.rectangle("fill",
+ gfx.setColor(colors.bg)
+ gfx.rectangle("fill",
+ 0,
0,
- start_y,
drawableWidth,
apparentHeight * fh)
end
local highlight = input.highlight
local visible = vc:get_visible()
- G.setFont(self.cfg.font)
+ gfx.setFont(self.cfg.font)
drawBackground()
- self.statusline:draw(status, apparentLines, time)
+
+ gfx.push('all')
+ self.statusline:draw(status, 0)
+ gfx.pop()
if highlight then
local hl = highlight.hl
- if highlight.parse_err then
- --- grammar = 'lua'
- local color = colors.fg
- local perr = highlight.parse_err
- local el, ec
- if perr then
- el = perr.l
- ec = perr.c
- end
- for l, s in ipairs(visible) do
- local ln = l + vc.offset
- local tl = string.ulen(s)
-
- if not tl then return end
-
- for c = 1, tl do
- local char = string.usub(s, c, c)
-
- local hl_li = wrap_reverse[ln]
- local tlc = vc:translate_from_visible(Cursor(l, c))
-
- if tlc then
- local ci = (function()
- if hl[tlc.l] then
- return hl[tlc.l][tlc.c]
- end
- end)()
- if ci then
- color = Color[ci] or colors.fg
- end
- end
- if perr and ln > el or
- (ln == el and (c > ec or ec == 1)) then
- color = cf_colors.input.error
- end
- local selected = (function()
- local sel = input.selection
- local startl = sel.start and sel.start.l
- local endl = sel.fin and sel.fin.l
- if startl then
- local startc = sel.start.c
- local endc = sel.fin.c
- if startc and endc then
- if startl == endl then
- local sc = math.min(sel.start.c, sel.fin.c)
- local endi = math.max(sel.start.c, sel.fin.c)
- return l == startl and c >= sc and c < endi
- else
- return
- (l == startl and c >= sel.start.c) or
- (l > startl and l < endl) or
- (l == endl and c < sel.fin.c)
- end
- end
+ local perr = highlight.parse_err
+ local el, ec
+ if perr then
+ el = perr.l
+ ec = perr.c
+ end
+ for l, s in ipairs(visible) do
+ local ln = l + vc.offset
+ local tl = string.ulen(s)
+ if not tl then return end
+
+ for c = 1, tl do
+ local char = string.usub(s, c, c)
+ local color = colors.fg
+
+ local hl_li = wrap_reverse[ln]
+ local tlc = vc:translate_from_visible(Cursor(l, c))
+
+ if tlc then
+ local ci = (function()
+ if hl[tlc.l] then
+ return hl[tlc.l][tlc.c]
end
end)()
- --- number of lines back from EOF
- local diffset = #text - vc.range.fin
- local of = overflow
- --- push any further lines down to display phantom line
- if ofpos and hl_li > cl then
- of = of - 1
+ if ci then
+ color = Color[ci] or colors.fg
end
- local dy = y - (-ln - diffset + 1 + of) * fh
- local dx = (c - 1) * fw
- ViewUtils.write_token(dy, dx,
- char, color, colors.bg, selected)
end
- end
- else
- --- grammar = 'md'
- for l, s in ipairs(visible) do
- local ln = l + vc.offset
- local tl = string.ulen(s)
- if not tl then return end
-
- for c = 1, tl do
- local char = string.usub(s, c, c)
- local color = colors.fg
-
- --- @diagnostic disable-next-line: param-type-mismatch
- local tlc = vc:translate_from_visible(Cursor(l, c))
-
- if tlc then
- local row = hl[tlc.l]
- local lex_t = row[tlc.c]
- if lex_t then
- color = Color[lex_t] or colors.fg
- end
- end
- local hl_li = wrap_reverse[ln]
-
- local selected = (function()
- local sel = input.selection
- local startl = sel.start and sel.start.l
- local endl = sel.fin and sel.fin.l
- if startl then
- local startc = sel.start.c
- local endc = sel.fin.c
- if startc and endc then
- if startl == endl then
- local sc = math.min(sel.start.c, sel.fin.c)
- local endi = math.max(sel.start.c, sel.fin.c)
- return l == startl and c >= sc and c < endi
- else
- return
- (l == startl and c >= sel.start.c) or
- (l > startl and l < endl) or
- (l == endl and c < sel.fin.c)
- end
+ if perr and ln > el or
+ (ln == el and (c > ec or ec == 1)) then
+ color = cf_colors.input.error
+ end
+ local selected = (function()
+ local sel = input.selection
+ local startl = sel.start and sel.start.l
+ local endl = sel.fin and sel.fin.l
+ if startl then
+ local startc = sel.start.c
+ local endc = sel.fin.c
+ if startc and endc then
+ if startl == endl then
+ local sc = math.min(sel.start.c, sel.fin.c)
+ local endi = math.max(sel.start.c, sel.fin.c)
+ return l == startl and c >= sc and c < endi
+ else
+ return
+ (l == startl and c >= sel.start.c) or
+ (l > startl and l < endl) or
+ (l == endl and c < sel.fin.c)
end
end
- end)()
- --- number of lines back from EOF
- local diffset = #text - vc.range.fin
- local of = overflow
- --- push any further lines down to display phantom line
- if ofpos and hl_li > cl then
- of = of - 1
end
- local dy = y - (-ln - diffset + 1 + of) * fh
- local dx = (c - 1) * fw
- ViewUtils.write_token(dy, dx,
- char, color, colors.bg, selected)
+ end)()
+ local of = calc_overflow(w, text, cursorInfo.cursor)
+ --- push any further lines down to display phantom line
+ if ofpos and hl_li > cl then
+ of = of - 1
end
+ local dy = (l) * fh
+ local dx = (c - 1) * fw
+ ViewUtils.write_token(dy, dx,
+ char, color, colors.bg, selected)
end
end
else
+ gfx.push('all')
+ gfx.setColor(colors.fg)
for l, str in ipairs(visible) do
- G.setColor(colors.fg)
- ViewUtils.write_line(l, str, start_y, 0, self.cfg)
+ ViewUtils.write_line(l, str, fh, 0, self.cfg)
end
+ gfx.pop()
end
drawCursor()
end
+--- @param err_text string[]
+function UserInputView:render_error(err_text)
+ local colors = self.cfg.colors
+ local fh = self.cfg.fh
+ local vpH = gfx.getHeight()
+
+ local inLines = #err_text
+ self.start_h = vpH - (inLines + 1) * fh
+ local drawableWidth = self.cfg.drawableWidth
+ local apparentHeight = #err_text
+ local start_y = fh -- statusline
+
+ local drawBackground = function()
+ gfx.setColor(colors.input.error_bg)
+ gfx.rectangle("fill",
+ 0,
+ fh,
+ drawableWidth,
+ apparentHeight * fh)
+ end
+
+ gfx.push('all')
+ drawBackground()
+
+ gfx.setColor(colors.input.error)
+
+ for l, str in ipairs(err_text) do
+ local breaks = 0 -- starting height is already calculated
+ ViewUtils.write_line(l, str, start_y, breaks, self.cfg)
+ end
+ gfx.pop()
+end
+
--- @param input InputDTO
---- @param time number?
-function UserInputView:draw(input, time)
+--- @param status Status
+function UserInputView:render(input, status)
+ local gfx = love.graphics
+ --- @diagnostic disable-next-line: undefined-field
+ if gfx.mock then return end
local err_text = input.wrapped_error or {}
local isError = string.is_non_empty_string_array(err_text)
- local colors = self.cfg.colors
- local fh = self.cfg.fh
- local h = self.cfg.h
-
- if isError then
- local drawableWidth = self.cfg.drawableWidth
- local inLines = #err_text
- local inHeight = inLines * fh
- local apparentHeight = #err_text
- local start_y = h - inHeight
- local drawBackground = function()
- G.setColor(colors.input.error_bg)
- G.rectangle("fill",
- 0,
- start_y,
- drawableWidth,
- apparentHeight * fh)
+ self.canvas:renderTo(function()
+ gfx.clear(0, 0, 0, 1)
+ if isError then
+ self:render_error(err_text)
+ else
+ self:render_input(input, status)
end
+ end)
+end
- drawBackground()
- G.setColor(colors.input.error)
- for l, str in ipairs(err_text) do
- local breaks = 0 -- starting height is already calculated
- ViewUtils.write_line(l, str, start_y, breaks, self.cfg)
- end
- else
- self:draw_input(input, time)
+--- Draw the pre-rendered canvas to screen
+function UserInputView:draw()
+ if not self.controller:is_oneshot() then
+ self.controller:update_view()
end
+ local b = self.cfg.statusline_border / 2
+ local h = self.start_h - b
+ gfx.push('all')
+ gfx.setBlendMode("replace")
+ love.graphics.draw(self.canvas, 0, h)
+ gfx.setBlendMode("alpha")
+ gfx.pop()
end
--- Whether the cursor is at limit, accounting for word wrap.
diff --git a/src/view/titleView.lua b/src/view/titleView.lua
index 5a4c8f42..27e2dd13 100644
--- a/src/view/titleView.lua
+++ b/src/view/titleView.lua
@@ -1,20 +1,20 @@
TitleView = {
draw = function(title, x, y, w, custom_font)
title = title or "LÖVEputer"
- local prev_font = G.getFont()
+ local prev_font = gfx.getFont()
local font = custom_font or prev_font
local fh = font:getHeight()
x = x or 0
- y = y or G.getHeight() - 2 * fh
- w = w or G.getWidth()
- G.setColor(Color[0])
- G.rectangle("fill", x, y, w, fh)
+ y = y or gfx.getHeight() - 2 * fh
+ w = w or gfx.getWidth()
+ gfx.setColor(Color[0])
+ gfx.rectangle("fill", x, y, w, fh)
local i = 1
local c = { 13, 12, 14, 10 }
for lx = w - fh, w - 4 * fh, -fh do
- G.setColor(Color[c[i]])
+ gfx.setColor(Color[c[i]])
i = i + 1
- G.polygon("fill",
+ gfx.polygon("fill",
lx,
y,
lx - fh,
@@ -24,14 +24,14 @@ TitleView = {
lx - fh,
y + fh)
end
- G.setColor(Color[15])
+ gfx.setColor(Color[15])
if custom_font then
- G.setFont(font)
+ gfx.setFont(font)
end
- G.print(title, x + fh, y)
+ gfx.print(title, x + fh, y)
if custom_font then
- G.setFont(prev_font)
+ gfx.setFont(prev_font)
end
end
}
diff --git a/src/view/view.lua b/src/view/view.lua
index 4cc29370..c5d04842 100644
--- a/src/view/view.lua
+++ b/src/view/view.lua
@@ -1,35 +1,61 @@
---- @type love.Image?
-local canvas_snapshot = nil
+local gfx = love.graphics
+
+local FPSfont = gfx.newFont("assets/fonts/fraps.otf", 24)
View = {
+ --- @type love.Image?
+ snapshot = nil,
prev_draw = nil,
main_draw = nil,
end_draw = nil,
--- @param C ConsoleController
--- @param CV ConsoleView
draw = function(C, CV)
- G.push('all')
+ gfx.push('all')
local terminal = C:get_terminal()
local canvas = C:get_canvas()
- local input = C.input:get_input()
- CV:draw(terminal, canvas, input, canvas_snapshot)
- G.pop()
- end,
-
- snap_canvas = function()
- -- G.captureScreenshot(os.time() .. ".png")
- if canvas_snapshot then
- View.clear_snapshot()
- collectgarbage()
- end
- G.captureScreenshot(function(img)
- canvas_snapshot = G.newImage(img)
- end)
+ CV:draw(terminal, canvas, View.snapshot)
+ gfx.pop()
end,
clear_snapshot = function()
- canvas_snapshot = nil
+ View.snapshot = nil
end,
+
+ drawFPS = function()
+ local pr = love.PROFILE
+ if type(pr) ~= 'table' then return end
+ if love.PROFILE.fpsc == 'off' then return end
+
+ local fps = tostring(love.timer.getFPS())
+ local mode = love.PROFILE.fpsc
+ local w = FPSfont:getWidth(fps)
+ local fh = FPSfont:getHeight()
+ local y = 10
+ local x
+ if mode == 'T_L'
+ or mode == 'T_L_B'
+ then
+ x = 10
+ elseif mode == 'T_R'
+ or mode == 'T_R_B'
+ then
+ x = gfx.getWidth() - 10 - w
+ end
+ gfx.push('all')
+ local prevCanvas = gfx.getCanvas()
+ gfx.setCanvas()
+ if mode == 'T_L_B' or mode == 'T_R_B' then
+ gfx.setColor(Color[Color.black])
+ gfx.rectangle("fill",
+ x, y, w, fh - 5)
+ end
+ gfx.setColor(Color[Color.yellow])
+ gfx.setFont(FPSfont)
+ gfx.print(fps, x, y)
+ gfx.setCanvas(prevCanvas)
+ gfx.pop()
+ end
}
--- @class ViewBase
diff --git a/tests/editor/buffer_spec.lua b/tests/editor/buffer_spec.lua
index 68e40c57..a61ece0d 100644
--- a/tests/editor/buffer_spec.lua
+++ b/tests/editor/buffer_spec.lua
@@ -107,7 +107,7 @@ print(sierpinski(4))]])
describe('lua', function()
local turtle = {
'--- @diagnostic disable',
- 'width, height = G.getDimensions()',
+ 'width, height = gfx.getDimensions()',
'midx = width / 2',
'midy = height / 2',
'incr = 5',
@@ -119,15 +119,15 @@ print(sierpinski(4))]])
'bg_color = Color.black',
'',
'local function drawHelp()',
- ' G.setColor(Color[Color.white])',
- ' G.print("Press [I] to open console", 20, 20)',
- ' G.print("Enter \'forward\', \'back\', \'left\', or \'right\' to move the turtle!", 20, 40)',
+ ' gfx.setColor(Color[Color.white])',
+ ' gfx.print("Press [I] to open console", 20, 20)',
+ ' gfx.print("Enter \'forward\', \'back\', \'left\', or \'right\' to move the turtle!", 20, 40)',
'end',
'',
'local function drawDebuginfo()',
- ' G.setColor(Color[debugColor])',
+ ' gfx.setColor(Color[debugColor])',
' local label = string.format("Turtle position: (%d, %d)", tx, ty)',
- ' G.print(label, width - 200, 20)',
+ ' gfx.print(label, width - 200, 20)',
'end',
'',
'function love.draw()',
@@ -212,13 +212,13 @@ print(sierpinski(4))]])
assert.same('', embuf:get_text_content()[1])
embuf:move_selection('down', 2)
assert.same(3, embuf:get_selection())
- assert.same({ 'width, height = G.getDimensions()' }, embuf:get_selected_text())
+ assert.same({ 'width, height = gfx.getDimensions()' }, embuf:get_selected_text())
local res = {
'',
'--- @diagnostic disable',
'',
- 'width, height = G.getDimensions()',
+ 'width, height = gfx.getDimensions()',
'midx = width / 2',
}
assert.same(3, embuf:get_selection())
@@ -229,7 +229,7 @@ print(sierpinski(4))]])
embuf:insert_newline()
assert.same(res, table.take(embuf:get_text_content(), 5))
embuf:move_selection('down')
- assert.same({ 'width, height = G.getDimensions()' }, embuf:get_selected_text())
+ assert.same({ 'width, height = gfx.getDimensions()' }, embuf:get_selected_text())
embuf:insert_newline()
assert.same(res, table.take(embuf:get_text_content(), 5))
end)
diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua
index 4e052c41..2e983e33 100644
--- a/tests/editor/editor_spec.lua
+++ b/tests/editor/editor_spec.lua
@@ -25,17 +25,6 @@ describe('Editor #editor', function()
'Turtle graphics game inspired the LOGO family of languages.',
'',
}
- --- @param w integer
- --- @param l integer?
- local function getMockConf(w, l)
- return {
- view = {
- drawableChars = w,
- lines = l or 16,
- input_max = 14
- },
- }
- end
--- @param cfg Config
--- @return EditorController
@@ -75,7 +64,7 @@ describe('Editor #editor', function()
describe('opens', function()
it('no wrap needed', function()
local w = 80
- local controller = wire(getMockConf(w))
+ local controller = wire(TU.mock_view_cfg(w))
local save = TU.get_save_function(turtle_doc)
controller:open('turtle', turtle_doc, save)
@@ -100,8 +89,7 @@ describe('Editor #editor', function()
local w = 16
love.state.app_state = 'editor'
- local controller, press = wire(getMockConf(w))
- local model = controller.model
+ local controller, press = wire(TU.mock_view_cfg(w))
local save = TU.get_save_function(turtle_doc)
@@ -130,7 +118,7 @@ describe('Editor #editor', function()
assert.same(start_sel - 1, buffer:get_selection())
mock.keystroke('up', press)
assert.same(start_sel - 2, buffer:get_selection())
- assert.same(turtle_doc[2], model.buffer:get_selected_text())
+ assert.same(turtle_doc[2], buffer:get_selected_text())
--- load it
local input = function()
return controller.input:get_text():items()
@@ -184,16 +172,17 @@ describe('Editor #editor', function()
describe('with scroll', function()
local l = 6
- local controller, _, view = wire(getMockConf(80, l))
+ local controller, _, view = wire(TU.mock_view_cfg(80, l))
local model = controller.model
local save = TU.get_save_function(sierpinski)
--- use it as plaintext for this test
controller:open('sierpinski.txt', sierpinski, save)
- view.buffer:open(model.buffer)
+ local buf = controller:get_active_buffer()
+ local bv = view:open(buf)
- local visible = view.buffer.content
- local scroll = view.buffer.SCROLL_BY
+ local visible = bv.content
+ local scroll = bv.SCROLL_BY
local off = #sierpinski - l + 1
local start_range = Range(off + 1, #sierpinski + 1)
@@ -201,7 +190,7 @@ describe('Editor #editor', function()
it('loads', function()
--- inital scroll is at EOF, meaning last l lines are visible
--- plus the phantom line
- assert.same(off, view.buffer.offset)
+ assert.same(off, bv:get_offset())
assert.same(start_range, visible.range)
end)
local base = Range(1, l)
@@ -237,8 +226,7 @@ describe('Editor #editor', function()
describe('with scroll and wrap', function()
local l = 6
- local controller, _, view = wire(getMockConf(27, l))
- local model = controller.model
+ local controller, _, view = wire(TU.mock_view_cfg(27, l))
local save = TU.get_save_function(sierpinski)
controller:open('sierpinski.txt', sierpinski, save)
@@ -249,11 +237,11 @@ describe('Editor #editor', function()
local buffer = controller:get_active_buffer()
--- @type BufferView
- local bv = view.buffer
- bv:open(model.buffer)
+ local bv = view:open(buffer)
+ -- bv:open(buffer)
- local visible = view.buffer.content
- local scroll = view.buffer.SCROLL_BY
+ local visible = bv.content
+ local scroll = bv.SCROLL_BY
local clen = visible:get_content_length()
local off = clen - l + 1
@@ -261,7 +249,7 @@ describe('Editor #editor', function()
it('loads', function()
--- inital scroll is at EOF, meaning last l lines are visible
--- plus the phantom line
- assert.same(off, view.buffer.offset)
+ assert.same(off, bv:get_offset())
assert.same(start_range, visible.range)
end)
local base = Range(1, l)
@@ -439,9 +427,7 @@ describe('Editor #editor', function()
--- end plaintext
describe('structured (lua) works', function()
- local l = 16
-
- local controller, press = wire(getMockConf(64, l))
+ local controller, press = wire(TU.mock_view_cfg())
local save, savefile = TU.get_save_function(sierpinski)
controller:open('sierpinski.lua', sierpinski, save)
diff --git a/tests/editor/visible_content_spec.lua b/tests/editor/visible_content_spec.lua
index 9ad20188..d6445c01 100644
--- a/tests/editor/visible_content_spec.lua
+++ b/tests/editor/visible_content_spec.lua
@@ -1,14 +1,69 @@
require("view.editor.visibleContent")
+require("model.input.cursor")
require("util.string.string")
-describe('VisibleContent #wrap', function()
+local md_ex = [[### Input validation
+
+As an extension to the user input functionality, `validated_input()` allows arbitrary user-specified filters.
+A "filter" is a function, which takes a string as input and returns a boolean value of whether it is valid and an optional `Error`.
+The `Error` is structure which contains the error message (`msg`), and the location the error comes from, with line and character fields (`l` and `c`).
+
+#### Helper functions
+
+* `string.ulen(s)` - as opposed to the builtin `len()`, this works for unicode strings
+* `string.usub(s, from, to)` - unicode substrings
+* `Char.is_alpha(c)` - is `c` a letter
+* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric)
+]]
+
+describe('VisibleContent #visible', function()
+ local md_text = string.lines(md_ex)
+ local w = 64
local turtle_doc = {
'',
'Turtle graphics game inspired the LOGO family of languages.',
'',
}
+ it('translates', function()
+ local visible = VisibleContent(w, md_text, 1, 8)
+ --- scroll to the top
+ visible:move_range(- #md_text)
+ local cur11 = Cursor()
+ local cur33 = Cursor(3, 3)
+ local cur3w = Cursor(3, w)
+ local cur3wp1 = Cursor(3, w + 1)
+ local cur44 = Cursor(4, 4)
+ assert.same(cur11, visible:translate_to_wrapped(cur11))
+ assert.same(cur33, visible:translate_to_wrapped(cur33))
+ assert.same(cur3w, visible:translate_to_wrapped(cur3w))
+ assert.same(Cursor(4, 1), visible:translate_to_wrapped(cur3wp1))
+
+ assert.same(cur33, visible:translate_from_visible(cur33))
+ local cur3_67 = Cursor(3, 3 + w)
+ local exp3_67 = Cursor(4, 3)
+ assert.same(exp3_67, visible:translate_to_wrapped(cur3_67))
+
+ --- scroll to bottom
+ visible:to_end()
+ -- #01: ''
+ -- #02: '* `string.ulen(s)` - as opposed to the builtin `len()`, this wor'
+ -- #03: 'ks for unicode strings'
+ -- #04: '* `string.usub(s, from, to)` - unicode substrings'
+ -- #05: '* `Char.is_alpha(c)` - is `c` a letter'
+ -- #06: '* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric'
+ -- #07: ')'
+ -- #08: ''
+ assert.same(Cursor(9, 3 + w),
+ visible:translate_from_visible(cur33))
+ assert.same(Cursor(10, 4),
+ visible:translate_from_visible(cur44))
+ assert.is_nil(visible:translate_from_visible(Cursor(5, 40)))
+ local cur71 = Cursor(7, 1)
+ assert.same(Cursor(12, 65),
+ visible:translate_from_visible(cur71))
+ end)
local os_max = 8
local input_max = 16
diff --git a/tests/input/user_input_view_spec.lua b/tests/input/user_input_view_spec.lua
index ad2ae602..8b3f5bba 100644
--- a/tests/input/user_input_view_spec.lua
+++ b/tests/input/user_input_view_spec.lua
@@ -3,15 +3,11 @@ require("model.input.userInputModel")
require("controller.userInputController")
require("view.input.userInputView")
+TU = require('tests.testutil')
+
describe("input view spec #input", function()
- local w = 24
- local mockConf = {
- view = {
- drawableChars = w,
- lines = 16,
- input_max = 14
- },
- }
+ local w = 8
+ local mockConf = TU.mock_view_cfg(w)
mock = require("tests.mock")
local love = {
diff --git a/tests/interpreter/analyzer_inputs.lua b/tests/interpreter/analyzer_inputs.lua
index 4987c615..bf55daab 100644
--- a/tests/interpreter/analyzer_inputs.lua
+++ b/tests/interpreter/analyzer_inputs.lua
@@ -1,7 +1,7 @@
--- @param s str
---- @param defs Assignment[]
+--- @param semi SemanticInfo
--- @return table {string[], string[]}
-local prep = function(s, defs)
+local prep = function(s, semi)
local orig = (function()
if type(s) == 'string' then
return string.lines(s)
@@ -14,7 +14,7 @@ local prep = function(s, defs)
end
end)()
- return { orig, defs }
+ return { orig, semi }
end
local table1 = prep({
@@ -30,7 +30,7 @@ local table1 = prep({
' z = 2,',
' 3,',
'}',
-}, {
+}, SemanticInfo({
{ name = 't', line = 1, type = 'local' },
{ name = 't.ty', line = 2, type = 'field' },
{ name = 't2', line = 4, type = 'global' },
@@ -38,13 +38,13 @@ local table1 = prep({
{ name = 't2.w2', line = 6, type = 'field' },
{ name = 'a', line = 8, type = 'global' },
{ name = 'a.z', line = 10, type = 'field' },
-})
+}, {}))
local table2 = prep({
'tmp = {}',
'tmp[1] = 2',
-}, {
+}, SemanticInfo({
{ name = 'tmp', line = 1, type = 'global' }
-})
+}, {}))
local simple = {
--- sets
@@ -53,21 +53,22 @@ local simple = {
'y = 3',
'x = 3',
'w, ww = 10, 11',
- }, {
+ }, SemanticInfo({
{ line = 1, name = 'x', type = 'global', },
{ line = 2, name = 'y', type = 'global', },
{ line = 3, name = 'x', type = 'global', },
{ line = 4, name = 'w', type = 'global', },
{ line = 4, name = 'ww', type = 'global', },
- }),
+ }, {})),
+
prep({
'local l = 1',
'local x, y = 2, 3',
- }, {
+ }, SemanticInfo({
{ line = 1, name = 'l', type = 'local', },
{ line = 2, name = 'x', type = 'local', },
{ line = 2, name = 'y', type = 'local', },
- }),
+ }, {})),
--- tables
table1,
-- table2,
@@ -75,35 +76,35 @@ local simple = {
prep({
'function drawBackground()',
'end',
- }, {
+ }, SemanticInfo({
{ line = 1, name = 'drawBackground', type = 'function', },
- }),
+ }, {})),
prep({
'function love.draw()',
' draw()',
'end',
- }, {
+ }, SemanticInfo({
{ line = 1, name = 'love.draw', type = 'function', },
- }),
+ }, {})),
prep({
'function love.handlers.keypressed()',
'end',
- }, {
+ }, SemanticInfo({
{ line = 1, name = 'love.handlers.keypressed', type = 'function', },
- }),
+ }, {})),
prep({
'local function drawBody()',
'end',
- }, {
+ }, SemanticInfo({
{ name = 'drawBody', line = 1, type = 'function' }
- }),
+ }, {})),
--- methods
prep({
'function M:draw()',
'end',
- }, {
+ }, SemanticInfo({
{ name = 'M:draw', line = 1, type = 'method' }
- }),
+ }, {})),
}
local sierpinski = [[function sierpinski(depth)
@@ -210,7 +211,7 @@ end
local fullclock = [[
--- @diagnostic disable: duplicate-set-field,lowercase-global
-width, height = G.getDimensions()
+width, height = gfx.getDimensions()
midx = width / 2
midy = height / 2
@@ -232,7 +233,7 @@ s = 0
math.randomseed(os.time())
color = math.random(7)
bg_color = math.random(7)
-font = G.newFont(72)
+font = gfx.newFont(72)
local function pad(i)
return string.format("%02d", i)
@@ -249,15 +250,15 @@ function getTimestamp()
end
function love.draw()
- G.setColor(Color[color + Color.bright])
- G.setBackgroundColor(Color[bg_color])
- G.setFont(font)
+ gfx.setColor(Color[color + Color.bright])
+ gfx.setBackgroundColor(Color[bg_color])
+ gfx.setFont(font)
local text = getTimestamp()
local l = string.len(text)
local off_x = l * font:getWidth(' ')
local off_y = font:getHeight() / 2
- G.print(text, midx - off_x, midy - off_y, 0, 1, 1)
+ gfx.print(text, midx - off_x, midy - off_y, 0, 1, 1)
end
function love.update(dt)
@@ -295,14 +296,14 @@ end
]]
local full = {
- prep(sierpinski, {
+ prep(sierpinski, SemanticInfo({
{ line = 1, name = 'sierpinski', type = 'function', },
{ line = 2, name = 'lines', type = 'global', },
{ line = 4, name = 'sp', type = 'global', },
{ line = 5, name = 'tmp', type = 'global', },
{ line = 10, name = 'lines', type = 'global', },
- }),
- prep(clock, {
+ }, {})),
+ prep(clock, SemanticInfo({
{ line = 1, name = 'love.draw', type = 'function', },
{ line = 5, name = 'love.update', type = 'function', },
{ line = 6, name = 't', type = 'global', },
@@ -312,8 +313,8 @@ local full = {
{ line = 16, name = 'love.keyreleased', type = 'function', },
{ line = 19, name = 'bg_color', type = 'global', },
{ line = 21, name = 'color', type = 'global', },
- }),
- prep(meta, {
+ }, {})),
+ prep(meta, SemanticInfo({
{ line = 3, name = 'M:extract_comments', type = 'method', },
{ line = 4, name = 'lfi', type = 'local', },
{ line = 5, name = 'lla', type = 'local', },
@@ -346,8 +347,8 @@ local full = {
{ line = 32, name = 'li.multiline', type = 'field', },
{ line = 33, name = 'li.position', type = 'field', },
{ line = 34, name = 'li.prepend_newline', type = 'field', },
- }),
- prep(fullclock, {
+ }, {})),
+ prep(fullclock, SemanticInfo({
{ line = 2, name = 'width', type = 'global', },
{ line = 2, name = 'height', type = 'global', },
{ line = 3, name = 'midx', type = 'global', },
@@ -386,10 +387,31 @@ local full = {
{ line = 69, name = 'bg_color', type = 'global', },
{ line = 71, name = 'color', type = 'global', },
{ line = 75, name = 'love.keyreleased', type = 'function', },
- }),
+ }, {})),
+}
+
+local req = {
+ prep({
+ 'require ("math")',
+ 'x = sin(pi)',
+ }, SemanticInfo({
+ { name = 'x', line = 2, type = 'global' }
+ }, {
+ { name = 'math', line = 1 }
+ })),
+ prep({
+ 'require("action")',
+ 'print "req test"',
+ 'require("drawing")',
+ }, SemanticInfo({
+ }, {
+ { name = 'action', line = 1 },
+ { name = 'drawing', line = 3 },
+ })),
}
return {
- { 'simple', simple },
- { 'full', full },
+ { 'simple', simple },
+ { 'full', full },
+ { 'require', req },
}
diff --git a/tests/interpreter/analyzer_spec.lua b/tests/interpreter/analyzer_spec.lua
index 11d16417..37d62aec 100644
--- a/tests/interpreter/analyzer_spec.lua
+++ b/tests/interpreter/analyzer_spec.lua
@@ -96,7 +96,7 @@ describe('analyzer #analyzer', function()
---@diagnostic disable-next-line: param-type-mismatch
local semDB = analyzer.analyze(r)
it('matches ' .. i, function()
- assert.same(output, semDB.assignments)
+ assert.same(output, semDB)
end)
else
Log.warn('syntax error in input #' .. i)
diff --git a/tests/mock.lua b/tests/mock.lua
index 5d53fae8..360369f7 100644
--- a/tests/mock.lua
+++ b/tests/mock.lua
@@ -20,12 +20,24 @@ local mods = {
M = 'lalt',
}
+local W = 1024
+local H = 600
+
--- @param t love
local function mock_love(t)
local love = {
keyboard = {
isDown = function(k) return held[k] end
- }
+ },
+ graphics = {
+ mock = true,
+ getWidth = function() return W end,
+ getHeight = function() return H end,
+ getDimensions = function() return W, H end,
+ newCanvas = function() end,
+ setCanvas = function() end,
+ clear = function() end,
+ },
}
for k, v in pairs(t) do
love[k] = v
diff --git a/tests/testutil.lua b/tests/testutil.lua
index 7c1e4c1b..846b1fb1 100644
--- a/tests/testutil.lua
+++ b/tests/testutil.lua
@@ -1,6 +1,9 @@
require('util.table')
require('util.string.string')
+local wrap = 64
+
+local noop = function() end
--- @param init str
--- @return fun(str): boolean, string?
--- @return reftable handle
@@ -17,6 +20,24 @@ local get_save_function = function(init)
return save, handle
end
+--- @param w integer?
+--- @param l integer?
+local function getMockConf(w, l)
+ return {
+ view = {
+ drawableChars = w or wrap,
+ lines = l or 16,
+ input_max = 14,
+ fh = 32,
+ },
+ }
+end
+
return {
- get_save_function = get_save_function
+ get_save_function = get_save_function,
+ noop = noop,
+ LINES = 16,
+ SCROLL_BY = 8,
+ w = wrap,
+ mock_view_cfg = getMockConf,
}
diff --git a/tests/util/visible_spec.lua b/tests/util/visible_spec.lua
deleted file mode 100644
index 97fa932f..00000000
--- a/tests/util/visible_spec.lua
+++ /dev/null
@@ -1,63 +0,0 @@
-require("view.editor.visibleContent")
-require("model.input.cursor")
-require("util.string.string")
-require("util.debug")
-
-local md_ex = [[### Input validation
-
-As an extension to the user input functionality, `validated_input()` allows arbitrary user-specified filters.
-A "filter" is a function, which takes a string as input and returns a boolean value of whether it is valid and an optional `Error`.
-The `Error` is structure which contains the error message (`msg`), and the location the error comes from, with line and character fields (`l` and `c`).
-
-#### Helper functions
-
-* `string.ulen(s)` - as opposed to the builtin `len()`, this works for unicode strings
-* `string.usub(s, from, to)` - unicode substrings
-* `Char.is_alpha(c)` - is `c` a letter
-* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric)
-]]
-local text = string.lines(md_ex)
-local w = 64
-
-describe('VisibleContent #visible', function()
- local visible = VisibleContent(w, text, 1, 8)
-
- -- Log.debug(Debug.terse_ast(visible, true, 'lua'))
- it('translates', function()
- --- scroll to the top
- visible:move_range(- #text)
- local cur11 = Cursor()
- local cur33 = Cursor(3, 3)
- local cur3w = Cursor(3, w)
- local cur3wp1 = Cursor(3, w + 1)
- local cur44 = Cursor(4, 4)
- assert.same(cur11, visible:translate_to_wrapped(cur11))
- assert.same(cur33, visible:translate_to_wrapped(cur33))
- assert.same(cur3w, visible:translate_to_wrapped(cur3w))
- assert.same(Cursor(4, 1), visible:translate_to_wrapped(cur3wp1))
-
- assert.same(cur33, visible:translate_from_visible(cur33))
- local cur3_67 = Cursor(3, 3 + w)
- local exp3_67 = Cursor(4, 3)
- assert.same(exp3_67, visible:translate_to_wrapped(cur3_67))
-
- --- scroll to bottom
- visible:to_end()
- -- #01: ''
- -- #02: '* `string.ulen(s)` - as opposed to the builtin `len()`, this wor'
- -- #03: 'ks for unicode strings'
- -- #04: '* `string.usub(s, from, to)` - unicode substrings'
- -- #05: '* `Char.is_alpha(c)` - is `c` a letter'
- -- #06: '* `Char.is_alnum(c)` - is `c` a letter or a number (alphanumeric'
- -- #07: ')'
- -- #08: ''
- assert.same(Cursor(9, 3 + w),
- visible:translate_from_visible(cur33))
- assert.same(Cursor(10, 4),
- visible:translate_from_visible(cur44))
- assert.is_nil(visible:translate_from_visible(Cursor(5, 40)))
- local cur71 = Cursor(7, 1)
- assert.same(Cursor(12, 65),
- visible:translate_from_visible(cur71))
- end)
-end)