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 ![capitalized](./doc/interface/hello_cap.apng) Happy with the modifications now, we can quit by pressing -Ctrl-Shift-Q +Ctrl-Shift-S ![quit](./doc/interface/quit_editor.apng) 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)