diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 1e5140f2..02b089bb 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -4,36 +4,114 @@ local command = require "core.command" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" +local CommandView = require "core.commandview" local LogView = require "core.logview" local View = require "core.view" local Object = require "core.object" +---@alias StatusView.styledtext table +---@alias StatusView.itemscb fun():StatusView.styledtext,StatusView.styledtext +---@alias StatusView.clickcb fun(button: string, x: number, y: number) +---@alias StatusView.predicate fun():boolean +---A status bar implementation for lite, check core.status_view. +---@class StatusView : View +---@field private items StatusView.item[] +---@field private hovered_item StatusView.item +---@field private message_timeout number +---@field private message StatusView.styledtext +---@field private tooltip_mode boolean +---@field private tooltip StatusView.styledtext local StatusView = View:extend() +---@class StatusView.item +---@field predicate StatusView.predicate +---@field items StatusView.itemscb +---@field onclick StatusView.clickcb +---@field tooltip string | nil +---@field lx number +---@field lw number +---@field rx number +---@field rw number +---@field active boolean +StatusView.item = {} + +---Space separator +---@type string StatusView.separator = " " + +---Pipe separator +---@type string StatusView.separator2 = " | " +---Constructor function StatusView:new() StatusView.super.new(self) self.message_timeout = 0 self.message = {} self.tooltip_mode = false self.tooltip = {} + self.items = {} + self.hovered_item = {} + self.pointer = {x = 0, y = 0} + + self:add_item( + function() + return core.active_view:is(DocView) and + not core.active_view:is(CommandView) + end, + self.get_doc_items + ) + + self:add_item("core.commandview", self.get_command_items) end -function StatusView:on_mouse_pressed() - core.set_active_view(core.last_active_view) - if system.get_time() < self.message_timeout - and not core.active_view:is(LogView) then - command.perform "core:open-log" +---Adds an item to be rendered in the status bar. +---@param predicate string | table | StatusView.predicate : +---A coindition to evaluate if the item should be displayed. If a string +---is given it is treated as a file that returns a valid object which is +---checked against the current active view, the sames applies if a table is +---given. A function can be used instead to perform a custom evaluation. +---@param itemscb StatusView.itemscb : +---This function should return two tables of StatusView.styledtext elements +---for both left and right, empty tables are allowed. +---@param pos? integer : +---The position in which to insert the given item on the internal table, +---a value of -1 inserts the item at the end which is the default. A value +---of 1 will insert the item at the beggining. +---@param onclick? StatusView.clickcb Executed when user clicks the item +---@param tooltip? string Displayed when mouse hovers the item +function StatusView:add_item(predicate, itemscb, pos, onclick, tooltip) + predicate = predicate or always_true + 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 + ---@type StatusView.item + local item = { + predicate = predicate, + items = itemscb, + onclick = onclick, + tooltip = tooltip + } + pos = type(pos) == "nil" and -1 or math.abs(tonumber(pos)) + if pos == -1 then + table.insert(self.items, item) + else + table.insert(self.items, pos, item) end - return true end +---Shows a message for a predefined amount of time. +---@param icon string +---@param icon_color renderer.color +---@param text string function StatusView:show_message(icon, icon_color, text) self.message = { icon_color, style.icon_font, icon, @@ -43,30 +121,27 @@ function StatusView:show_message(icon, icon_color, text) end +---Activates tooltip mode displaying only the given +---text until StatusView:remove_tooltip() is called. +---@param text string | StatusView.styledtext function StatusView:show_tooltip(text) - self.tooltip = { text } + self.tooltip = type(text) == "table" and text or { text } self.tooltip_mode = true end +---Deactivates tooltip mode. function StatusView:remove_tooltip() self.tooltip_mode = false end -function StatusView:update() - self.size.y = style.font:get_height() + style.padding.y * 2 - - if system.get_time() < self.message_timeout then - self.scroll.to.y = self.size.y - else - self.scroll.to.y = 0 - end - - StatusView.super.update(self) -end - - +---Helper function to draw the styled text. +---@param self StatusView +---@param items StatusView.styledtext +---@param x number +---@param y number +---@param draw_fn fun(font,color,text,align, x,y,w,h):number local function draw_items(self, items, x, y, draw_fn) local font = style.font local color = style.text @@ -85,11 +160,20 @@ local function draw_items(self, items, x, y, draw_fn) end +---Helper function to calculate the width of text by using it as part of +---the helper function draw_items(). +---@param font renderer.font +---@param text string +---@param x number local function text_width(font, _, text, _, x) return x + font:get_width(text) end +---Draws a table of styled text on the status bar starting on the left or right. +---@param items StatusView.styledtext +---@param right_align boolean +---@param yoffset number function StatusView:draw_items(items, right_align, yoffset) local x, y = self:get_content_offset() y = y + (yoffset or 0) @@ -104,38 +188,78 @@ function StatusView:draw_items(items, right_align, yoffset) end -function StatusView:get_items() - if getmetatable(core.active_view) == DocView then - local dv = core.active_view - local line, col = dv.doc:get_selection() - local dirty = dv.doc:is_dirty() - local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info() - local indent_label = (indent_type == "hard") and "tabs: " or "spaces: " - local indent_size_str = tostring(indent_size) .. (indent_confirmed and "" or "*") or "unknown" +---Draw the tooltip of a given status bar item. +---@param item StatusView.item +function StatusView:draw_item_tooltip(item) + core.root_view:defer_draw(function() + local text = item.tooltip + local w = style.font:get_width(text) + local h = style.font:get_height() + local x = self.pointer.x - (w / 2) - (style.padding.x * 2) - return { - dirty and style.accent or style.text, style.icon_font, "f", - style.dim, style.font, self.separator2, style.text, - dv.doc.filename and style.text or style.dim, dv.doc:get_name(), - style.text, - self.separator, - "line: ", line, - self.separator, - col > config.line_limit and style.accent or style.text, "col: ", col, - style.text, - self.separator, - string.format("%.f%%", line / #dv.doc.lines * 100), - }, { - style.text, indent_label, indent_size, - style.dim, self.separator2, style.text, - style.icon_font, "g", - style.font, style.dim, self.separator2, style.text, - #dv.doc.lines, " lines", - self.separator, - dv.doc.crlf and "CRLF" or "LF" - } - end + if x < 0 then x = 0 end + if x + w + (style.padding.x * 2) > self.size.x then + x = self.size.x - w - (style.padding.x * 2) + end + renderer.draw_rect( + x, + self.position.y - h - (style.padding.y * 2), + w + (style.padding.x * 2), + h + (style.padding.y * 2), + style.background3 + ) + + renderer.draw_text( + style.font, + text, + x + style.padding.x, + self.position.y - h - style.padding.y, + style.text + ) + end) +end + + +---The predefined status bar items displayed when a document view is active. +---@return table left +---@return table right +function StatusView:get_doc_items() + local dv = core.active_view + local line, col = dv.doc:get_selection() + local dirty = dv.doc:is_dirty() + local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info() + local indent_label = (indent_type == "hard") and "tabs: " or "spaces: " + local indent_size_str = tostring(indent_size) .. (indent_confirmed and "" or "*") or "unknown" + + return { + dirty and style.accent or style.text, style.icon_font, "f", + style.dim, style.font, self.separator2, style.text, + dv.doc.filename and style.text or style.dim, dv.doc:get_name(), + style.text, + self.separator, + "line: ", line, + self.separator, + col > config.line_limit and style.accent or style.text, "col: ", col, + style.text, + self.separator, + string.format("%.f%%", line / #dv.doc.lines * 100), + }, { + style.text, indent_label, indent_size, + style.dim, self.separator2, style.text, + style.icon_font, "g", + style.font, style.dim, self.separator2, style.text, + #dv.doc.lines, " lines", + self.separator, + dv.doc.crlf and "CRLF" or "LF" + } +end + + +---The predefined status bar items displayed when a command view is active. +---@return table left +---@return table right +function StatusView:get_command_items() return {}, { style.icon_font, "g", style.font, style.dim, self.separator2, @@ -145,6 +269,175 @@ function StatusView:get_items() end +---Helper function to copy a styled text table into another. +---@param t1 StatusView.styledtext +---@param t2 StatusView.styledtext +local function table_add(t1, t2) + for i, value in ipairs(t2) do + table.insert(t1, value) + end +end + + +---Get the styled text that will be displayed on the left side or +---right side of the status bar checking their predicates and performing +---positioning calculations for proper functioning of tooltips and clicks. +---@return StatusView.styledtext left +---@return StatusView.styledtext right +function StatusView:get_items_from_list() + local left, right = {}, {} + + local x = self:get_content_offset() + + local rx = x + self.size.x + local lx = x + style.padding.x + local rw, lw = 0, 0 + + ---@class items + ---@field item StatusView.item + local items = {} + + -- calculate left and right width + for _, item in ipairs(self.items) do + if item.predicate(self) then + item.active = true + + local litems, ritems = item.items(self) + + if #litems > 0 then + item.lw = draw_items(self, litems, 0, 0, text_width) + item.lx = lx + lw = lw + item.lw + lx = lx + item.lw + end + + if #ritems > 0 then + item.rw = draw_items(self, ritems, 0, 0, text_width) + item.rx = rx + rw = rw + item.rw + rx = rx + item.rw + end + + if #litems > 0 or #ritems > 0 then + table.insert(items, { + item = item, + left = litems, + right = ritems + }) + end + else + item.active = false + end + end + + -- load deprecated items for compatibility + local dleft, dright = self:get_items(true) + if #dright > 0 then + rw = rw + draw_items(self, dright, 0, 0, text_width) + end + + rw = rw < (self.size.x / 2) and rw or self.size.x / 2 + + for _, item in ipairs(items) do + if #item.left > 0 then + table_add(left, item.left) + end + if #item.right > 0 then + -- re-calculate x position now that we have the total width + item.item.rx = item.item.rx - rw - style.padding.x + table_add(right, item.right) + end + end + + table_add(left, dleft) + table_add(right, dright) + + return left, right +end + + +---Older method of retrieving the status bar items and which is now +---deprecated in favour of core.status_view:add_item(). +---@deprecated +---@param nowarn boolean +---@return table left +---@return table right +function StatusView:get_items(nowarn) + if not nowarn and not self.get_items_warn then + core.error( + "Overriding StatusView:get_items() is deprecated, " + .. "use core.status_view:add_item() instead." + ) + self.get_items_warn = true + end + return {}, {} +end + + +function StatusView:on_mouse_pressed() + core.set_active_view(core.last_active_view) + if system.get_time() < self.message_timeout + and not core.active_view:is(LogView) then + command.perform "core:open-log" + end + return true +end + + +function StatusView:on_mouse_moved(x, y, dx, dy) + StatusView.super.on_mouse_moved(self, x, y, dx, dy) + + if y < self.position.y then self.hovered_item = {} return end + + for _, item in ipairs(self.items) do + if item.onclick and item.active then + if + (item.lx and x > item.lx and (item.lx + item.lw) > x) + or + (item.rx and x > item.rx and (item.rx + item.rw) > x) + then + self.pointer.x = x + self.pointer.y = y + if self.hovered_item ~= item then + self.hovered_item = item + end + return + end + end + end + self.hovered_item = {} +end + + +function StatusView:on_mouse_released(button, x, y) + StatusView.super.on_mouse_released(self, button, x, y) + + if y < self.position.y or not self.hovered_item.onclick then return end + + local item = self.hovered_item + if + (item.lx and x > item.lx and (item.lx + item.lw) > x) + or + (item.rx and x > item.rx and (item.rx + item.rw) > x) + then + self.hovered_item.onclick(button, x, y) + end +end + + +function StatusView:update() + self.size.y = style.font:get_height() + style.padding.y * 2 + + if system.get_time() < self.message_timeout then + self.scroll.to.y = self.size.y + else + self.scroll.to.y = 0 + end + + StatusView.super.update(self) +end + + function StatusView:draw() self:draw_background(style.background2) @@ -155,9 +448,12 @@ function StatusView:draw() if self.tooltip_mode then self:draw_items(self.tooltip) else - local left, right = self:get_items() + local left, right = self:get_items_from_list() self:draw_items(left) self:draw_items(right, true) + if self.hovered_item.tooltip then + self:draw_item_tooltip(self.hovered_item) + end end end