From b046afccf9b7294331ec35c4d77f51aab4a7a378 Mon Sep 17 00:00:00 2001 From: Francesco Date: Thu, 3 Jun 2021 22:49:37 +0200 Subject: [PATCH] Scale fonts context menu (#246) * Retrieve scale plugin from lite-plugins * New implementation of scale plugin and font C API Introduce two new C API functions, renderer.font.get_size and set_size respectively to get the font size and to set the size to a new value. Using these functions we don't need to know the name of the font but we can just change their size. Adapt the scale plugin to use the new C API function with minor adaptations in the logic. Use smaller step to scale fonts. Rename font_desc_free function, previous name was misleading as only the cached resources are freed. * Add contextmenu plugin from takase From https://github.com/takase1121/lite-contextmenu Adapted to show font scaling commands and find/replace commands. i# testing.lua * Fix the cursor flickering with contextmenu To avoid flickering of the cursor when using the context menu we add a new function `core.request_cursor` that just take note of the cursor requested. The cursor will be actually changed only in root_view:draw() method only when all the drawing operations are done. This means the cursor will be changed only once per frame and only the most recent cursor change request will take effect. * Remove unneeded scale plugin return functions --- data/core/init.lua | 5 + data/core/rootview.lua | 14 +- data/plugins/contextmenu.lua | 285 +++++++++++++++++++++++++++++++++++ data/plugins/scale.lua | 100 ++++++++++++ src/api/renderer_font.c | 20 ++- src/fontdesc.c | 3 +- src/fontdesc.h | 2 +- 7 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 data/plugins/contextmenu.lua create mode 100644 data/plugins/scale.lua diff --git a/data/core/init.lua b/data/core/init.lua index a3e6eba6..c16307c8 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1073,6 +1073,11 @@ function core.blink_reset() end +function core.request_cursor(value) + core.cursor_change_req = value +end + + function core.on_error(err) -- write error to file local fp = io.open(USERDIR .. "/error.txt", "wb") diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 8b3a91ae..b01303c5 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -783,7 +783,7 @@ end function RootView:on_mouse_moved(x, y, dx, dy) if core.active_view == core.nag_view then - system.set_cursor("arrow") + core.request_cursor("arrow") core.active_view:on_mouse_moved(x, y, dx, dy) return end @@ -808,14 +808,14 @@ function RootView:on_mouse_moved(x, y, dx, dy) local div = self.root_node:get_divider_overlapping_point(x, y) local tab_index = node and node:get_tab_overlapping_point(x, y) if node and node:get_scroll_button_index(x, y) then - system.set_cursor("arrow") + core.request_cursor("arrow") elseif div then local axis = (div.type == "hsplit" and "x" or "y") if div.a:is_resizable(axis) and div.b:is_resizable(axis) then - system.set_cursor(div.type == "hsplit" and "sizeh" or "sizev") + core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") end elseif tab_index then - system.set_cursor("arrow") + core.request_cursor("arrow") if self.dragged_node and self.dragged_node ~= tab_index then local tab = node.views[self.dragged_node] table.remove(node.views, self.dragged_node) @@ -823,7 +823,7 @@ function RootView:on_mouse_moved(x, y, dx, dy) self.dragged_node = tab_index end else - system.set_cursor(node.active_view.cursor) + core.request_cursor(node.active_view.cursor) end end @@ -858,6 +858,10 @@ function RootView:draw() local t = table.remove(self.deferred_draws) t.fn(table.unpack(t)) end + if core.cursor_change_req then + system.set_cursor(core.cursor_change_req) + core.cursor_change_req = nil + end end diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua new file mode 100644 index 00000000..e7a27e7d --- /dev/null +++ b/data/plugins/contextmenu.lua @@ -0,0 +1,285 @@ +-- mod-version:1 -- lite-xl 1.16 +local core = require "core" +local common = require "core.common" +local config = require "core.config" +local command = require "core.command" +local keymap = require "core.keymap" +local style = require "core.style" +local Object = require "core.object" +local RootView = require "core.rootview" + +local border_width = 1 +local divider_width = 1 +local DIVIDER = {} + +local ContextMenu = Object:extend() + +function ContextMenu:new() + self.itemset = {} + self.show_context_menu = false + self.selected = -1 + self.height = 0 + self.position = { x = 0, y = 0 } +end + +local function get_item_size(item) + local lw, lh + if item == DIVIDER then + lw = 0 + lh = divider_width + else + lw = style.font:get_width(item.text) + if item.info then + lw = lw + style.padding.x + style.font:get_width(item.info) + end + lh = style.font:get_height() + style.padding.y + end + return lw, lh +end + +function ContextMenu:register(predicate, items) + if type(predicate) == "string" then + predicate = require(predicate) + end + if type(predicate) == "table" then + local class = predicate + predicate = function() return core.active_view:is(class) end + end + + local width, height = 0, 0 --precalculate the size of context menu + for i, item in ipairs(items) do + if item ~= DIVIDER then + item.info = keymap.reverse_map[item.command] + end + local lw, lh = get_item_size(item) + width = math.max(width, lw) + height = height + lh + end + width = width + style.padding.x * 2 + items.width, items.height = width, height + table.insert(self.itemset, { predicate = predicate, items = items }) +end + +function ContextMenu:show(x, y) + self.items = nil + for _, items in ipairs(self.itemset) do + if items.predicate(x, y) then + self.items = items.items + break + end + end + + if self.items then + local w, h = self.items.width, self.items.height + + -- by default the box is opened on the right and below + if x + w >= core.root_view.size.x then + x = x - w + end + if y + h >= core.root_view.size.y then + y = y - h + end + + self.position.x, self.position.y = x, y + self.show_context_menu = true + return true + end + return false +end + +function ContextMenu:hide() + self.show_context_menu = false + self.items = nil + self.selected = -1 + self.height = 0 +end + +function ContextMenu:each_item() + local x, y, w = self.position.x, self.position.y, self.items.width + local oy = y + return coroutine.wrap(function() + for i, item in ipairs(self.items) do + local _, lh = get_item_size(item) + if y - oy > self.height then break end + coroutine.yield(i, item, x, y, w, lh) + y = y + lh + end + end) +end + +function ContextMenu:on_mouse_moved(px, py) + if not self.show_context_menu then return end + + self.selected = -1 + for i, item, x, y, w, h in self:each_item() do + if px > x and px <= x + w and py > y and py <= y + h then + self.selected = i + break + end + end + if self.selected >= 0 then + core.request_cursor("arrow") + end +end + +function ContextMenu:on_selected(item) + if type(item.command) == "string" then + command.perform(item.command) + else + item.command() + end +end + +function ContextMenu:on_mouse_pressed(button, x, y, clicks) + local selected = (self.items or {})[self.selected] + local caught = false + + self:hide() + if button == "left" then + if selected then + self:on_selected(selected) + caught = true + end + end + + if button == "right" then + caught = self:show(x, y) + end + return caught +end + +-- copied from core.docview +function ContextMenu:move_towards(t, k, dest, rate) + if type(t) ~= "table" then + return self:move_towards(self, t, k, dest, rate) + end + local val = t[k] + if not config.transitions or math.abs(val - dest) < 0.5 then + t[k] = dest + else + rate = rate or 0.5 + if config.fps ~= 60 or config.animation_rate ~= 1 then + local dt = 60 / config.fps + rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) + end + t[k] = common.lerp(val, dest, rate) + end + if val ~= dest then + core.redraw = true + end +end + +function ContextMenu:update() + if self.show_context_menu then + self:move_towards("height", self.items.height) + end +end + +function ContextMenu:draw() + if not self.show_context_menu then return end + core.root_view:defer_draw(self.draw_context_menu, self) +end + +function ContextMenu:draw_context_menu() + if not self.items then return end + local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height + + renderer.draw_rect( + bx - border_width, + by - border_width, + bw + (border_width * 2), + bh + (border_width * 2), + style.divider + ) + renderer.draw_rect(bx, by, bw, bh, style.background3) + + for i, item, x, y, w, h in self:each_item() do + if item == DIVIDER then + renderer.draw_rect(x, y, w, h, style.caret) + else + if i == self.selected then + renderer.draw_rect(x, y, w, h, style.selection) + end + + common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h) + if item.info then + common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h) + end + end + end +end + + +local menu = ContextMenu() +local root_view_on_mouse_pressed = RootView.on_mouse_pressed +local root_view_on_mouse_moved = RootView.on_mouse_moved +local root_view_update = RootView.update +local root_view_draw = RootView.draw + +function RootView:on_mouse_moved(...) + root_view_on_mouse_moved(self, ...) + menu:on_mouse_moved(...) +end + +-- this function is mostly copied from lite-xl's source +function RootView:on_mouse_pressed(button, x,y, clicks) + local div = self.root_node:get_divider_overlapping_point(x, y) + if div then + self.dragged_divider = div + return + end + local node = self.root_node:get_child_overlapping_point(x, y) + if node.hovered_scroll_button > 0 then + node:scroll_tabs(node.hovered_scroll_button) + return + end + local idx = node:get_tab_overlapping_point(x, y) + if idx then + if button == "middle" or node.hovered_close == idx then + node:close_view(self.root_node, node.views[idx]) + else + self.dragged_node = idx + node:set_active_view(node.views[idx]) + end + else + core.set_active_view(node.active_view) + -- send to context menu first + if not menu:on_mouse_pressed(button, x, y, clicks) then + node.active_view:on_mouse_pressed(button, x, y, clicks) + end + end +end + +function RootView:update(...) + root_view_update(self, ...) + menu:update() +end + +function RootView:draw(...) + root_view_draw(self, ...) + menu:draw() +end + +command.add(nil, { + ["context:show"] = function() + menu:show(core.active_view.position.x, core.active_view.position.y) + end +}) + +keymap.add { + ["menu"] = "context:show" +} + +if require("plugins.scale") then + menu:register("core.docview", { + { text = "Font +", command = "scale:increase" }, + { text = "Font -", command = "scale:decrease" }, + { text = "Font Reset", command = "scale:reset" }, + DIVIDER, + { text = "Find", command = "find-replace:find" }, + { text = "Replace", command = "find-replace:replace" }, + DIVIDER, + { text = "Find Pattern", command = "find-replace:find-pattern" }, + { text = "Replace Pattern", command = "find-replace:replace-pattern" }, + }) +end diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua new file mode 100644 index 00000000..11064235 --- /dev/null +++ b/data/plugins/scale.lua @@ -0,0 +1,100 @@ +-- mod-version:1 -- lite-xl 1.16 +local core = require "core" +local common = require "core.common" +local command = require "core.command" +local config = require "core.config" +local keymap = require "core.keymap" +local style = require "core.style" +local RootView = require "core.rootview" +local CommandView = require "core.commandview" + +config.scale_mode = "code" +config.scale_use_mousewheel = true + +local scale_level = 0 +local scale_steps = 0.05 + +local current_scale = SCALE +local default_scale = SCALE + +local function set_scale(scale) + scale = common.clamp(scale, 0.2, 6) + + -- save scroll positions + local scrolls = {} + for _, view in ipairs(core.root_view.root_node:get_children()) do + local n = view:get_scrollable_size() + if n ~= math.huge and not view:is(CommandView) and n > view.size.y then + scrolls[view] = view.scroll.y / (n - view.size.y) + end + end + + local s = scale / current_scale + current_scale = scale + + if config.scale_mode == "ui" then + SCALE = scale + + style.padding.x = style.padding.x * s + style.padding.y = style.padding.y * s + style.divider_size = style.divider_size * s + style.scrollbar_size = style.scrollbar_size * s + style.caret_width = style.caret_width * s + style.tab_width = style.tab_width * s + + for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do + renderer.font.set_size(style[name], s * style[name]:get_size()) + end + else + renderer.font.set_size(style.code_font, s * style.code_font:get_size()) + end + + -- restore scroll positions + for view, n in pairs(scrolls) do + view.scroll.y = n * (view:get_scrollable_size() - view.size.y) + view.scroll.to.y = view.scroll.y + end + + core.redraw = true +end + + +local on_mouse_wheel = RootView.on_mouse_wheel + +function RootView:on_mouse_wheel(d, ...) + if keymap.modkeys["ctrl"] and config.scale_use_mousewheel then + if d < 0 then command.perform "scale:decrease" end + if d > 0 then command.perform "scale:increase" end + else + return on_mouse_wheel(self, d, ...) + end +end + +local function res_scale() + scale_level = 0 + set_scale(default_scale) +end + +local function inc_scale() + scale_level = scale_level + 1 + set_scale(default_scale + scale_level * scale_steps) +end + +local function dec_scale() + scale_level = scale_level - 1 + set_scale(default_scale + scale_level * scale_steps) +end + + +command.add(nil, { + ["scale:reset" ] = function() res_scale() end, + ["scale:decrease"] = function() dec_scale() end, + ["scale:increase"] = function() inc_scale() end, +}) + +keymap.add { + ["ctrl+0"] = "scale:reset", + ["ctrl+-"] = "scale:decrease", + ["ctrl+="] = "scale:increase", +} + diff --git a/src/api/renderer_font.c b/src/api/renderer_font.c index 47ea97a3..f510da70 100644 --- a/src/api/renderer_font.c +++ b/src/api/renderer_font.c @@ -65,7 +65,7 @@ static int f_set_tab_size(lua_State *L) { static int f_gc(lua_State *L) { FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - font_desc_free(self); + font_desc_clear(self); return 0; } @@ -104,6 +104,22 @@ static int f_get_height(lua_State *L) { } +static int f_get_size(lua_State *L) { + FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); + lua_pushnumber(L, self->size); + return 1; +} + + +static int f_set_size(lua_State *L) { + FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); + float new_size = luaL_checknumber(L, 2); + font_desc_clear(self); + self->size = new_size; + return 0; +} + + static const luaL_Reg lib[] = { { "__gc", f_gc }, { "load", f_load }, @@ -112,6 +128,8 @@ static const luaL_Reg lib[] = { { "get_width_subpixel", f_get_width_subpixel }, { "get_height", f_get_height }, { "subpixel_scale", f_subpixel_scale }, + { "get_size", f_get_size }, + { "set_size", f_set_size }, { NULL, NULL } }; diff --git a/src/fontdesc.c b/src/fontdesc.c index d1d0825f..44460a6d 100644 --- a/src/fontdesc.c +++ b/src/fontdesc.c @@ -18,11 +18,12 @@ void font_desc_init(FontDesc *font_desc, const char *filename, float size, unsig font_desc->cache_last_index = 0; /* Normally no need to initialize. */ } -void font_desc_free(FontDesc *font_desc) { +void font_desc_clear(FontDesc *font_desc) { for (int i = 0; i < font_desc->cache_length; i++) { ren_free_font(font_desc->cache[i].font); } font_desc->cache_length = 0; + font_desc->cache_last_index = 0; } void font_desc_set_tab_size(FontDesc *font_desc, int tab_size) { diff --git a/src/fontdesc.h b/src/fontdesc.h index 2f4702ab..bf591801 100644 --- a/src/fontdesc.h +++ b/src/fontdesc.h @@ -26,7 +26,7 @@ void font_desc_init(FontDesc *font_desc, const char *filename, float size, unsig int font_desc_alloc_size(const char *filename); int font_desc_get_tab_size(FontDesc *font_desc); void font_desc_set_tab_size(FontDesc *font_desc, int tab_size); -void font_desc_free(FontDesc *font_desc); +void font_desc_clear(FontDesc *font_desc); RenFont *font_desc_get_font_at_scale(FontDesc *font_desc, int scale); #endif