1260 lines
36 KiB
Lua
1260 lines
36 KiB
Lua
local core = require "core"
|
|
local common = require "core.common"
|
|
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 core.statusview.styledtext table<integer, renderer.font|renderer.color|string>
|
|
---@alias core.statusview.position '"left"' | '"right"'
|
|
|
|
---A status bar implementation for lite, check core.status_view.
|
|
---@class core.statusview : core.view
|
|
---@field super core.view
|
|
---@field items core.statusview.item[]
|
|
---@field active_items core.statusview.item[]
|
|
---@field hovered_item core.statusview.item
|
|
---@field message_timeout number
|
|
---@field message core.statusview.styledtext
|
|
---@field tooltip_mode boolean
|
|
---@field tooltip core.statusview.styledtext
|
|
---@field left_width number
|
|
---@field right_width number
|
|
---@field r_left_width number
|
|
---@field r_right_width number
|
|
---@field left_xoffset number
|
|
---@field right_xoffset number
|
|
---@field dragged_panel '""' | core.statusview.position
|
|
---@field hovered_panel '""' | core.statusview.position
|
|
---@field hide_messages boolean
|
|
local StatusView = View:extend()
|
|
|
|
---Space separator
|
|
---@type string
|
|
StatusView.separator = " "
|
|
|
|
---Pipe separator
|
|
---@type string
|
|
StatusView.separator2 = " | "
|
|
|
|
---@alias core.statusview.item.separator
|
|
---|>`StatusView.separator`
|
|
---| `StatusView.separator2`
|
|
|
|
---@alias core.statusview.item.predicate fun():boolean
|
|
---@alias core.statusview.item.onclick fun(button: string, x: number, y: number)
|
|
---@alias core.statusview.item.get_item fun(self: core.statusview.item):core.statusview.styledtext?,core.statusview.styledtext?
|
|
---@alias core.statusview.item.ondraw fun(x, y, h, hovered: boolean, calc_only?: boolean):number
|
|
|
|
---@class core.statusview.item : core.object
|
|
---@field name string
|
|
---@field predicate core.statusview.item.predicate
|
|
---@field alignment core.statusview.item.alignment
|
|
---@field tooltip string
|
|
---@field command string | nil @Command to perform when the item is clicked.
|
|
---Function called when item is clicked and no command is set.
|
|
---@field on_click core.statusview.item.onclick | nil
|
|
---Custom drawing that when passed calc true should return the needed width for
|
|
---drawing and when false should draw.
|
|
---@field on_draw core.statusview.item.ondraw | nil
|
|
---@field background_color renderer.color | nil
|
|
---@field background_color_hover renderer.color | nil
|
|
---@field visible boolean
|
|
---@field separator core.statusview.item.separator
|
|
---@field active boolean
|
|
---@field x number
|
|
---@field w number
|
|
---@field cached_item core.statusview.styledtext
|
|
local StatusViewItem = Object:extend()
|
|
|
|
---Available StatusViewItem options.
|
|
---@class core.statusview.item.options : table
|
|
---A condition to evaluate if the item should be displayed. If a string
|
|
---is given it is treated as a require import that should return a valid object
|
|
---which is checked against the current active view, the sames applies if a
|
|
---table is given. A function that returns a boolean can be used instead to
|
|
---perform a custom evaluation, setting to nil means always evaluates to true.
|
|
---@field predicate string | table | core.statusview.item.predicate
|
|
---A unique name to identify the item on the status bar.
|
|
---@field name string @A unique name to identify the item on the status bar.
|
|
---@field alignment core.statusview.item.alignment
|
|
---A function that should return a core.statusview.styledtext element,
|
|
---returning an empty table is allowed.
|
|
---@field get_item core.statusview.item.get_item
|
|
---The name of a valid registered command or a callback function to execute
|
|
---when the item is clicked.
|
|
---@field command string | core.statusview.item.onclick | nil
|
|
---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.
|
|
---@field position? integer
|
|
---@field tooltip? string @Text displayed when mouse hovers the item.
|
|
---@field visible? boolean @Flag to show or hide the item
|
|
---The type of separator rendered to the right of the item if another item
|
|
---follows it.
|
|
---@field separator? core.statusview.item.separator
|
|
|
|
---Flag to tell the item should me aligned on left side of status bar.
|
|
---@type integer
|
|
StatusViewItem.LEFT = 1
|
|
|
|
---Flag to tell the item should me aligned on right side of status bar.
|
|
---@type integer
|
|
StatusViewItem.RIGHT = 2
|
|
|
|
---@alias core.statusview.item.alignment
|
|
---|>`StatusView.Item.LEFT`
|
|
---| `StatusView.Item.RIGHT`
|
|
|
|
---Constructor
|
|
---@param options core.statusview.item.options
|
|
function StatusViewItem:new(options)
|
|
self:set_predicate(options.predicate)
|
|
self.name = options.name
|
|
self.alignment = options.alignment or StatusView.Item.LEFT
|
|
self.command = type(options.command) == "string" and options.command or nil
|
|
self.tooltip = options.tooltip or ""
|
|
self.on_click = type(options.command) == "function" and options.command or nil
|
|
self.on_draw = nil
|
|
self.background_color = nil
|
|
self.background_color_hover = nil
|
|
self.visible = options.visible == nil and true or options.visible
|
|
self.active = false
|
|
self.x = 0
|
|
self.w = 0
|
|
self.separator = options.separator or StatusView.separator
|
|
self.get_item = options.get_item
|
|
end
|
|
|
|
---Called by the status bar each time that the item needs to be rendered,
|
|
---if on_draw() is set this function is obviated.
|
|
---@return core.statusview.styledtext
|
|
function StatusViewItem:get_item() return {} end
|
|
|
|
---Do not show the item on the status bar.
|
|
function StatusViewItem:hide() self.visible = false end
|
|
|
|
---Show the item on the status bar.
|
|
function StatusViewItem:show() self.visible = true end
|
|
|
|
---A condition to evaluate if the item should be displayed. If a string
|
|
---is given it is treated as a require import that should return a valid object
|
|
---which is checked against the current active view, the sames applies if a
|
|
---table is given. A function that returns a boolean can be used instead to
|
|
---perform a custom evaluation, setting to nil means always evaluates to true.
|
|
---@param predicate string | table | core.statusview.item.predicate
|
|
function StatusViewItem:set_predicate(predicate)
|
|
self.predicate = command.generate_predicate(predicate)
|
|
end
|
|
|
|
---@type core.statusview.item
|
|
StatusView.Item = StatusViewItem
|
|
|
|
|
|
---Predicated used on the default docview widgets.
|
|
---@return boolean
|
|
local function predicate_docview()
|
|
return core.active_view:is(DocView)
|
|
and not core.active_view:is(CommandView)
|
|
end
|
|
|
|
|
|
---Constructor
|
|
function StatusView:new()
|
|
StatusView.super.new(self)
|
|
self.message_timeout = 0
|
|
self.message = {}
|
|
self.tooltip_mode = false
|
|
self.tooltip = {}
|
|
self.items = {}
|
|
self.active_items = {}
|
|
self.hovered_item = {}
|
|
self.pointer = {x = 0, y = 0}
|
|
self.left_width = 0
|
|
self.right_width = 0
|
|
self.r_left_width = 0
|
|
self.r_right_width = 0
|
|
self.left_xoffset = 0
|
|
self.right_xoffset = 0
|
|
self.dragged_panel = ""
|
|
self.hovered_panel = ""
|
|
self.hide_messages = false
|
|
self.visible = true
|
|
|
|
self:register_docview_items()
|
|
self:register_command_items()
|
|
end
|
|
|
|
local clicks = -1
|
|
local gx, gy, dx, dy, gc = 0, 0, 2, -2, { table.unpack(style.text) }
|
|
|
|
---The predefined status bar items displayed when a document view is active.
|
|
function StatusView:register_docview_items()
|
|
if self:get_item("doc:file") then return end
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:file",
|
|
alignment = StatusView.Item.LEFT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
return {
|
|
dv.doc:is_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, common.home_encode(dv.doc:get_name())
|
|
}
|
|
end
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:position",
|
|
alignment = StatusView.Item.LEFT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
local line, col = dv.doc:get_selection()
|
|
local _, indent_size = dv.doc:get_indent_info()
|
|
-- Calculating tabs when the doc is using the "hard" indent type.
|
|
local ntabs = 0
|
|
local last_idx = 0
|
|
while last_idx < col do
|
|
local s, e = string.find(dv.doc.lines[line], "\t", last_idx, true)
|
|
if s and s < col then
|
|
ntabs = ntabs + 1
|
|
last_idx = e + 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
col = col + ntabs * (indent_size - 1)
|
|
return {
|
|
style.text, line, ":",
|
|
col > config.line_limit and style.accent or style.text, col,
|
|
style.text
|
|
}
|
|
end,
|
|
command = "doc:go-to-line",
|
|
tooltip = "line : column"
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:position-percent",
|
|
alignment = StatusView.Item.LEFT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
local line = dv.doc:get_selection()
|
|
return {
|
|
string.format("%.f%%", line / #dv.doc.lines * 100)
|
|
}
|
|
end,
|
|
tooltip = "caret position"
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:selections",
|
|
alignment = StatusView.Item.LEFT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
local nsel = math.floor(#dv.doc.selections / 4)
|
|
if nsel > 1 then
|
|
return { style.text, nsel, " selections" }
|
|
end
|
|
|
|
return {}
|
|
end
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:indentation",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
local indent_type, indent_size, indent_confirmed = dv.doc:get_indent_info()
|
|
local indent_label = (indent_type == "hard") and "tabs: " or "spaces: "
|
|
return {
|
|
style.text, indent_label, indent_size,
|
|
indent_confirmed and "" or "*"
|
|
}
|
|
end,
|
|
command = function(button, x, y)
|
|
if button == "left" then
|
|
command.perform "indent:set-file-indent-size"
|
|
elseif button == "right" then
|
|
command.perform "indent:set-file-indent-type"
|
|
end
|
|
end,
|
|
separator = self.separator2
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:stats",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
return config.stonks == nil and {} or {
|
|
style.text,
|
|
type(config.stonks) == "table" and config.stonks.font or style.icon_font,
|
|
type(config.stonks) == "table" and config.stonks.icon or ( config.stonks and "g" or "h" ),
|
|
}
|
|
end,
|
|
separator = self.separator2,
|
|
command = function(button, x, y)
|
|
if button == "left" then
|
|
clicks = clicks + 1
|
|
elseif button == "right" then
|
|
clicks = -1
|
|
end
|
|
gx, gy = x, y
|
|
end
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:lines",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
return {
|
|
style.text, #dv.doc.lines, " lines",
|
|
}
|
|
end,
|
|
separator = self.separator2
|
|
})
|
|
|
|
self:add_item({
|
|
predicate = predicate_docview,
|
|
name = "doc:line-ending",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
local dv = core.active_view
|
|
return {
|
|
style.text, dv.doc.crlf and "CRLF" or "LF"
|
|
}
|
|
end,
|
|
command = "doc:toggle-line-ending"
|
|
})
|
|
|
|
self:add_item {
|
|
predicate = predicate_docview,
|
|
name = "doc:overwrite-mode",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
return {
|
|
style.text, core.active_view.doc.overwrite and "OVR" or "INS"
|
|
}
|
|
end,
|
|
command = "doc:toggle-overwrite",
|
|
separator = StatusView.separator2
|
|
}
|
|
end
|
|
|
|
|
|
---The predefined status bar items displayed when a command view is active.
|
|
function StatusView:register_command_items()
|
|
if self:get_item("command:files") then return end
|
|
|
|
self:add_item({
|
|
predicate = "core.commandview",
|
|
name = "command:files",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
return {
|
|
style.icon_font, "g",
|
|
style.font, style.dim, self.separator2,
|
|
style.text, #core.docs, style.text, " / ",
|
|
#core.project_files, " files"
|
|
}
|
|
end
|
|
})
|
|
end
|
|
|
|
|
|
---Set a position to the best match according to total available items.
|
|
---@param self core.statusview
|
|
---@param position integer
|
|
---@param alignment core.statusview.item.alignment
|
|
---@return integer position
|
|
local function normalize_position(self, position, alignment)
|
|
local offset = 0
|
|
local items_count = 0
|
|
local left = self:get_items_list(1)
|
|
local right = self:get_items_list(2)
|
|
if alignment == 2 then
|
|
items_count = #right
|
|
offset = #left
|
|
else
|
|
items_count = #left
|
|
end
|
|
if position == 0 then
|
|
position = offset + 1
|
|
elseif position < 0 then
|
|
position = offset + items_count + (position + 2)
|
|
else
|
|
position = offset + position
|
|
end
|
|
if position < 1 then
|
|
position = offset + 1
|
|
elseif position > #left + #right then
|
|
position = offset + items_count + 1
|
|
end
|
|
return position
|
|
end
|
|
|
|
|
|
---Adds an item to be rendered in the status bar.
|
|
---@param options core.statusview.item.options
|
|
---@return core.statusview.item
|
|
function StatusView:add_item(options)
|
|
assert(self:get_item(options.name) == nil, "status item already exists: " .. options.name)
|
|
---@type core.statusview.item
|
|
local item = StatusView.Item(options)
|
|
table.insert(self.items, normalize_position(self, options.position or -1, options.alignment), item)
|
|
return item
|
|
end
|
|
|
|
|
|
---Get an item object associated to a name or nil if not found.
|
|
---@param name string
|
|
---@return core.statusview.item | nil
|
|
function StatusView:get_item(name)
|
|
for _, item in ipairs(self.items) do
|
|
if item.name == name then return item end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
---Get a list of items.
|
|
---@param alignment? core.statusview.item.alignment
|
|
---@return core.statusview.item[]
|
|
function StatusView:get_items_list(alignment)
|
|
if alignment then
|
|
local items = {}
|
|
for _, item in ipairs(self.items) do
|
|
if item.alignment == alignment then
|
|
table.insert(items, item)
|
|
end
|
|
end
|
|
return items
|
|
end
|
|
return self.items
|
|
end
|
|
|
|
|
|
---Move an item to a different position.
|
|
---@param name string
|
|
---@param position integer Can be negative value to position in reverse order
|
|
---@param alignment? core.statusview.item.alignment
|
|
---@return boolean moved
|
|
function StatusView:move_item(name, position, alignment)
|
|
assert(name, "no name provided")
|
|
assert(position, "no position provided")
|
|
local item = nil
|
|
for pos, it in ipairs(self.items) do
|
|
if it.name == name then
|
|
item = table.remove(self.items, pos)
|
|
break
|
|
end
|
|
end
|
|
if item then
|
|
if alignment then
|
|
item.alignment = alignment
|
|
end
|
|
position = normalize_position(self, position, item.alignment)
|
|
table.insert(self.items, position, item)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
---Remove an item from the status view.
|
|
---@param name string
|
|
---@return core.statusview.item removed_item
|
|
function StatusView:remove_item(name)
|
|
local item = nil
|
|
for pos, it in ipairs(self.items) do
|
|
if it.name == name then
|
|
item = table.remove(self.items, pos)
|
|
break
|
|
end
|
|
end
|
|
return item
|
|
end
|
|
|
|
|
|
---Order the items by name
|
|
---@param names table<integer, string>
|
|
function StatusView:order_items(names)
|
|
local removed_items = {}
|
|
for _, name in ipairs(names) do
|
|
local item = self:remove_item(name)
|
|
if item then table.insert(removed_items, item) end
|
|
end
|
|
|
|
for i, item in ipairs(removed_items) do
|
|
table.insert(self.items, i, item)
|
|
end
|
|
end
|
|
|
|
|
|
---Hide the status bar
|
|
function StatusView:hide()
|
|
self.visible = false
|
|
end
|
|
|
|
|
|
---Show the status bar
|
|
function StatusView:show()
|
|
self.visible = true
|
|
end
|
|
|
|
|
|
---Toggle the visibility of the status bar
|
|
function StatusView:toggle()
|
|
self.visible = not self.visible
|
|
end
|
|
|
|
|
|
---Hides the given items from the status view or all if no names given.
|
|
---@param names? table<integer, string> | string
|
|
function StatusView:hide_items(names)
|
|
if type(names) == "string" then
|
|
names = {names}
|
|
end
|
|
if not names then
|
|
for _, item in ipairs(self.items) do
|
|
item:hide()
|
|
end
|
|
return
|
|
end
|
|
for _, name in ipairs(names) do
|
|
local item = self:get_item(name)
|
|
if item then item:hide() end
|
|
end
|
|
end
|
|
|
|
|
|
---Shows the given items from the status view or all if no names given.
|
|
---@param names? table<integer, string> | string
|
|
function StatusView:show_items(names)
|
|
if type(names) == "string" then
|
|
names = {names}
|
|
end
|
|
if not names then
|
|
for _, item in ipairs(self.items) do
|
|
item:show()
|
|
end
|
|
return
|
|
end
|
|
for _, name in ipairs(names) do
|
|
local item = self:get_item(name)
|
|
if item then item:show() end
|
|
end
|
|
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)
|
|
if not self.visible or self.hide_messages then return end
|
|
self.message = {
|
|
icon_color, style.icon_font, icon,
|
|
style.dim, style.font, StatusView.separator2, style.text, text
|
|
}
|
|
self.message_timeout = system.get_time() + config.message_timeout
|
|
end
|
|
|
|
|
|
---Enable or disable system wide messages on the status bar.
|
|
---@param enable boolean
|
|
function StatusView:display_messages(enable)
|
|
self.hide_messages = not enable
|
|
end
|
|
|
|
|
|
---Activates tooltip mode displaying only the given
|
|
---text until core.statusview:remove_tooltip() is called.
|
|
---@param text string | core.statusview.styledtext
|
|
function StatusView:show_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
|
|
|
|
|
|
---Helper function to draw the styled text.
|
|
---@param self core.statusview
|
|
---@param items core.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
|
|
|
|
for _, item in ipairs(items) do
|
|
if Object.is(item, renderer.font) then
|
|
font = item
|
|
elseif type(item) == "table" then
|
|
color = item
|
|
else
|
|
x = draw_fn(font, color, item, nil, x, y, 0, self.size.y)
|
|
end
|
|
end
|
|
|
|
return x
|
|
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 core.statusview.styledtext
|
|
---@param right_align? boolean
|
|
---@param xoffset? number
|
|
---@param yoffset? number
|
|
function StatusView:draw_items(items, right_align, xoffset, yoffset)
|
|
local x, y = self:get_content_offset()
|
|
x = x + (xoffset or 0)
|
|
y = y + (yoffset or 0)
|
|
if right_align then
|
|
local w = draw_items(self, items, 0, 0, text_width)
|
|
x = x + self.size.x - w - style.padding.x
|
|
draw_items(self, items, x, y, common.draw_text)
|
|
else
|
|
x = x + style.padding.x
|
|
draw_items(self, items, x, y, common.draw_text)
|
|
end
|
|
end
|
|
|
|
|
|
---Draw the tooltip of a given status bar item.
|
|
---@param item core.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)
|
|
|
|
if x < 0 then x = 0 end
|
|
if (x + w + (style.padding.x * 3)) > self.size.x then
|
|
x = self.size.x - w - (style.padding.x * 3)
|
|
end
|
|
|
|
renderer.draw_rect(
|
|
x + style.padding.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 * 2),
|
|
self.position.y - h - style.padding.y,
|
|
style.text
|
|
)
|
|
end)
|
|
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.warn(
|
|
"Overriding StatusView:get_items() is deprecated, "
|
|
.. "use core.status_view:add_item() instead."
|
|
)
|
|
self.get_items_warn = true
|
|
end
|
|
return {"{:dummy:}"}, {"{:dummy:}"}
|
|
end
|
|
|
|
|
|
---Helper function to copy a styled text table into another.
|
|
---@param t1 core.statusview.styledtext
|
|
---@param t2 core.statusview.styledtext
|
|
local function table_add(t1, t2)
|
|
for _, value in ipairs(t2) do
|
|
table.insert(t1, value)
|
|
end
|
|
end
|
|
|
|
|
|
---Helper function to merge deprecated items to a temp items table.
|
|
---@param destination table
|
|
---@param items core.statusview.styledtext
|
|
---@param alignment core.statusview.item.alignment
|
|
local function merge_deprecated_items(destination, items, alignment)
|
|
local start = true
|
|
local items_start, items_end = {}, {}
|
|
for i, value in ipairs(items) do
|
|
if value ~= "{:dummy:}" then
|
|
if start then
|
|
table.insert(items_start, i, value)
|
|
else
|
|
table.insert(items_end, value)
|
|
end
|
|
else
|
|
start = false
|
|
end
|
|
end
|
|
|
|
local position = alignment == StatusView.Item.LEFT and "left" or "right"
|
|
|
|
local item_start = StatusView.Item({
|
|
name = "deprecated:"..position.."-start",
|
|
alignment = alignment,
|
|
get_item = items_start
|
|
})
|
|
|
|
local item_end = StatusView.Item({
|
|
name = "deprecated:"..position.."-end",
|
|
alignment = alignment,
|
|
get_item = items_end
|
|
})
|
|
|
|
table.insert(destination, 1, item_start)
|
|
table.insert(destination, item_end)
|
|
end
|
|
|
|
|
|
---Append a space item into the given items list.
|
|
---@param self core.statusview
|
|
---@param destination core.statusview.item[]
|
|
---@param separator string
|
|
---@param alignment core.statusview.item.alignment
|
|
---@return core.statusview.item
|
|
local function add_spacing(self, destination, separator, alignment, x)
|
|
---@type core.statusview.item
|
|
local space = StatusView.Item({name = "space", alignment = alignment})
|
|
space.cached_item = separator == self.separator and {
|
|
style.text, separator
|
|
} or {
|
|
style.dim, separator
|
|
}
|
|
space.x = x
|
|
space.w = draw_items(self, space.cached_item, 0, 0, text_width)
|
|
|
|
table.insert(destination, space)
|
|
|
|
return space
|
|
end
|
|
|
|
|
|
---Remove starting and ending separators.
|
|
---@param self core.statusview
|
|
---@param styled_text core.statusview.styledtext
|
|
local function remove_spacing(self, styled_text)
|
|
if
|
|
not Object.is(styled_text[1], renderer.font)
|
|
and
|
|
type(styled_text[1]) == "table"
|
|
and
|
|
(
|
|
styled_text[2] == self.separator
|
|
or
|
|
styled_text[2] == self.separator2
|
|
)
|
|
then
|
|
table.remove(styled_text, 1)
|
|
table.remove(styled_text, 1)
|
|
end
|
|
|
|
if
|
|
not Object.is(styled_text[#styled_text-1], renderer.font)
|
|
and
|
|
type(styled_text[#styled_text-1]) == "table"
|
|
and
|
|
(
|
|
styled_text[#styled_text] == self.separator
|
|
or
|
|
styled_text[#styled_text] == self.separator2
|
|
)
|
|
then
|
|
table.remove(styled_text, #styled_text)
|
|
table.remove(styled_text, #styled_text)
|
|
end
|
|
end
|
|
|
|
|
|
---Set the active items that will be displayed on the left or right side
|
|
---of the status bar checking their predicates and performing positioning
|
|
---calculations for proper functioning of tooltips and clicks.
|
|
function StatusView:update_active_items()
|
|
local x = self:get_content_offset()
|
|
|
|
local rx = x + self.size.x
|
|
local lx = x
|
|
local rw, lw = 0, 0
|
|
|
|
self.active_items = {}
|
|
|
|
---@type core.statusview.item[]
|
|
local combined_items = {}
|
|
table_add(combined_items, self.items)
|
|
|
|
-- load deprecated items for compatibility
|
|
local dleft, dright = self:get_items(true)
|
|
merge_deprecated_items(combined_items, dleft, StatusView.Item.LEFT)
|
|
merge_deprecated_items(combined_items, dright, StatusView.Item.RIGHT)
|
|
|
|
local lfirst, rfirst = true, true
|
|
|
|
-- calculate left and right width
|
|
for _, item in ipairs(combined_items) do
|
|
item.cached_item = {}
|
|
if item.visible and item:predicate() then
|
|
local styled_text = type(item.get_item) == "function"
|
|
and item.get_item(item) or item.get_item
|
|
|
|
if #styled_text > 0 then
|
|
remove_spacing(self, styled_text)
|
|
end
|
|
|
|
if #styled_text > 0 or item.on_draw then
|
|
item.active = true
|
|
local hovered = self.hovered_item == item
|
|
if item.alignment == StatusView.Item.LEFT then
|
|
if not lfirst then
|
|
local space = add_spacing(
|
|
self, self.active_items, item.separator, item.alignment, lx
|
|
)
|
|
lw = lw + space.w
|
|
lx = lx + space.w
|
|
else
|
|
lfirst = false
|
|
end
|
|
item.w = item.on_draw and
|
|
item.on_draw(lx, self.position.y, self.size.y, hovered, true)
|
|
or
|
|
draw_items(self, styled_text, 0, 0, text_width)
|
|
item.x = lx
|
|
lw = lw + item.w
|
|
lx = lx + item.w
|
|
else
|
|
if not rfirst then
|
|
local space = add_spacing(
|
|
self, self.active_items, item.separator, item.alignment, rx
|
|
)
|
|
rw = rw + space.w
|
|
rx = rx + space.w
|
|
else
|
|
rfirst = false
|
|
end
|
|
item.w = item.on_draw and
|
|
item.on_draw(rx, self.position.y, self.size.y, hovered, true)
|
|
or
|
|
draw_items(self, styled_text, 0, 0, text_width)
|
|
item.x = rx
|
|
rw = rw + item.w
|
|
rx = rx + item.w
|
|
end
|
|
item.cached_item = styled_text
|
|
table.insert(self.active_items, item)
|
|
else
|
|
item.active = false
|
|
end
|
|
else
|
|
item.active = false
|
|
end
|
|
end
|
|
|
|
self.r_left_width, self.r_right_width = lw, rw
|
|
|
|
-- try to calc best size for left and right
|
|
if lw + rw + (style.padding.x * 4) > self.size.x then
|
|
if lw + (style.padding.x * 2) < self.size.x / 2 then
|
|
rw = self.size.x - lw - (style.padding.x * 3)
|
|
if rw > self.r_right_width then
|
|
lw = lw + (rw - self.r_right_width)
|
|
rw = self.r_right_width
|
|
end
|
|
elseif rw + (style.padding.x * 2) < self.size.x / 2 then
|
|
lw = self.size.x - rw - (style.padding.x * 3)
|
|
else
|
|
lw = self.size.x / 2 - (style.padding.x + style.padding.x / 2)
|
|
rw = self.size.x / 2 - (style.padding.x + style.padding.x / 2)
|
|
end
|
|
-- reposition left and right offsets when window is resized
|
|
if rw >= self.r_right_width then
|
|
self.right_xoffset = 0
|
|
elseif rw > self.right_xoffset + self.r_right_width then
|
|
self.right_xoffset = rw - self.r_right_width
|
|
end
|
|
if lw >= self.r_left_width then
|
|
self.left_xoffset = 0
|
|
elseif lw > self.left_xoffset + self.r_left_width then
|
|
self.left_xoffset = lw - self.r_left_width
|
|
end
|
|
else
|
|
self.left_xoffset = 0
|
|
self.right_xoffset = 0
|
|
end
|
|
|
|
self.left_width, self.right_width = lw, rw
|
|
|
|
for _, item in ipairs(self.active_items) do
|
|
if item.alignment == StatusView.Item.RIGHT then
|
|
-- re-calculate x position now that we have the total width
|
|
item.x = item.x - rw - (style.padding.x * 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
---Drag the given panel if possible.
|
|
---@param panel core.statusview.position
|
|
---@param dx number
|
|
function StatusView:drag_panel(panel, dx)
|
|
if panel == "left" and self.r_left_width > self.left_width then
|
|
local nonvisible_w = self.r_left_width - self.left_width
|
|
local new_offset = self.left_xoffset + dx
|
|
if new_offset >= 0 - nonvisible_w and new_offset <= 0 then
|
|
self.left_xoffset = new_offset
|
|
elseif dx < 0 then
|
|
self.left_xoffset = 0 - nonvisible_w
|
|
else
|
|
self.left_xoffset = 0
|
|
end
|
|
elseif panel == "right" and self.r_right_width > self.right_width then
|
|
local nonvisible_w = self.r_right_width - self.right_width
|
|
local new_offset = self.right_xoffset + dx
|
|
if new_offset >= 0 - nonvisible_w and new_offset <= 0 then
|
|
self.right_xoffset = new_offset
|
|
elseif dx < 0 then
|
|
self.right_xoffset = 0 - nonvisible_w
|
|
else
|
|
self.right_xoffset = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
---Return the currently hovered panel or empty string if none.
|
|
---@param x number
|
|
---@param y number
|
|
---@return string
|
|
function StatusView:get_hovered_panel(x, y)
|
|
if y >= self.position.y and x <= self.left_width + style.padding.x then
|
|
return "left"
|
|
end
|
|
return "right"
|
|
end
|
|
|
|
|
|
---@param item core.statusview.item
|
|
---@return number x
|
|
---@return number w
|
|
function StatusView:get_item_visible_area(item)
|
|
local item_ox = item.alignment == StatusView.Item.LEFT and
|
|
self.left_xoffset or self.right_xoffset
|
|
|
|
local item_x = item_ox + item.x + style.padding.x
|
|
local item_w = item.w
|
|
|
|
if item.alignment == StatusView.Item.LEFT then
|
|
if self.left_width - item_x > 0 and self.left_width - item_x < item.w then
|
|
item_w = (self.left_width + style.padding.x) - item_x
|
|
elseif self.left_width - item_x < 0 then
|
|
item_x = 0
|
|
item_w = 0
|
|
end
|
|
else
|
|
local rx = self.size.x - self.right_width - style.padding.x
|
|
if item_x < rx then
|
|
if item_x + item.w > rx then
|
|
item_x = rx
|
|
item_w = (item_x + item.w) - rx
|
|
else
|
|
item_x = 0
|
|
item_w = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
return item_x, item_w
|
|
end
|
|
|
|
|
|
|
|
function StatusView:on_mouse_pressed(button, x, y, clicks)
|
|
if not self.visible then return end
|
|
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"
|
|
else
|
|
if y >= self.position.y and button == "left" and clicks == 1 then
|
|
self.position.dx = x
|
|
if
|
|
self.r_left_width > self.left_width
|
|
or
|
|
self.r_right_width > self.right_width
|
|
then
|
|
self.dragged_panel = self:get_hovered_panel(x, y)
|
|
self.cursor = "hand"
|
|
end
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
|
|
function StatusView:on_mouse_left()
|
|
StatusView.super.on_mouse_left(self)
|
|
self.hovered_item = {}
|
|
end
|
|
|
|
|
|
function StatusView:on_mouse_moved(x, y, dx, dy)
|
|
if not self.visible then return end
|
|
StatusView.super.on_mouse_moved(self, x, y, dx, dy)
|
|
|
|
self.hovered_panel = self:get_hovered_panel(x, y)
|
|
|
|
if self.dragged_panel ~= "" then
|
|
self:drag_panel(self.dragged_panel, dx)
|
|
return
|
|
end
|
|
|
|
if y < self.position.y or system.get_time() <= self.message_timeout then
|
|
self.cursor = "arrow"
|
|
self.hovered_item = {}
|
|
return
|
|
end
|
|
|
|
for _, item in ipairs(self.items) do
|
|
if
|
|
item.visible and item.active
|
|
and
|
|
(item.command or item.on_click or item.tooltip ~= "")
|
|
then
|
|
local item_x, item_w = self:get_item_visible_area(item)
|
|
|
|
if x > item_x and (item_x + item_w) > x then
|
|
self.pointer.x = x
|
|
self.pointer.y = y
|
|
if self.hovered_item ~= item then
|
|
self.hovered_item = item
|
|
end
|
|
if item.command or item.on_click then
|
|
self.cursor = "hand"
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
self.cursor = "arrow"
|
|
self.hovered_item = {}
|
|
end
|
|
|
|
|
|
function StatusView:on_mouse_released(button, x, y)
|
|
if not self.visible then return end
|
|
StatusView.super.on_mouse_released(self, button, x, y)
|
|
|
|
if self.dragged_panel ~= "" then
|
|
self.dragged_panel = ""
|
|
self.cursor = "arrow"
|
|
if self.position.dx ~= x then
|
|
return
|
|
end
|
|
end
|
|
|
|
if y < self.position.y or not self.hovered_item.active then return end
|
|
|
|
local item = self.hovered_item
|
|
local item_x, item_w = self:get_item_visible_area(item)
|
|
|
|
if x > item_x and (item_x + item_w) > x then
|
|
if item.command then
|
|
command.perform(item.command)
|
|
elseif item.on_click then
|
|
item.on_click(button, x, y)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function StatusView:on_mouse_wheel(y, x)
|
|
if not self.visible or self.hovered_panel == "" then return end
|
|
if x ~= 0 then
|
|
self:drag_panel(self.hovered_panel, x * self.left_width / 10)
|
|
else
|
|
self:drag_panel(self.hovered_panel, y * self.left_width / 10)
|
|
end
|
|
end
|
|
|
|
|
|
function StatusView:update()
|
|
if not self.visible and self.size.y <= 0 then
|
|
return
|
|
elseif not self.visible and self.size.y > 0 then
|
|
self:move_towards(self.size, "y", 0, nil, "statusbar")
|
|
return
|
|
end
|
|
|
|
local height = style.font:get_height() + style.padding.y * 2;
|
|
|
|
if self.size.y + 1 < height then
|
|
self:move_towards(self.size, "y", height, nil, "statusbar")
|
|
else
|
|
self.size.y = height
|
|
end
|
|
|
|
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)
|
|
|
|
self:update_active_items()
|
|
end
|
|
|
|
|
|
---Retrieve the hover status and proper background color if any.
|
|
---@param self core.statusview
|
|
---@param item core.statusview.item
|
|
---@return boolean is_hovered
|
|
---@return renderer.color | nil color
|
|
local function get_item_bg_color(self, item)
|
|
local hovered = self.hovered_item == item
|
|
|
|
local item_bg = hovered
|
|
and item.background_color_hover or item.background_color
|
|
|
|
return hovered, item_bg
|
|
end
|
|
|
|
|
|
function StatusView:draw()
|
|
if not self.visible and self.size.y <= 0 then return end
|
|
|
|
self:draw_background(style.background2)
|
|
|
|
if self.message and system.get_time() <= self.message_timeout then
|
|
self:draw_items(self.message, false, 0, self.size.y)
|
|
else
|
|
if self.tooltip_mode then
|
|
self:draw_items(self.tooltip)
|
|
end
|
|
if #self.active_items > 0 then
|
|
--- draw left pane
|
|
core.push_clip_rect(
|
|
0, self.position.y,
|
|
self.left_width + style.padding.x, self.size.y
|
|
)
|
|
for _, item in ipairs(self.active_items) do
|
|
local item_x = self.left_xoffset + item.x + style.padding.x
|
|
local hovered, item_bg = get_item_bg_color(self, item)
|
|
if item.alignment == StatusView.Item.LEFT and not self.tooltip_mode then
|
|
if type(item_bg) == "table" then
|
|
renderer.draw_rect(
|
|
item_x, self.position.y,
|
|
item.w, self.size.y, item_bg
|
|
)
|
|
end
|
|
if item.on_draw then
|
|
core.push_clip_rect(item_x, self.position.y, item.w, self.size.y)
|
|
item.on_draw(item_x, self.position.y, self.size.y, hovered)
|
|
core.pop_clip_rect()
|
|
else
|
|
self:draw_items(item.cached_item, false, item_x - style.padding.x)
|
|
end
|
|
end
|
|
end
|
|
core.pop_clip_rect()
|
|
|
|
--- draw right pane
|
|
core.push_clip_rect(
|
|
self.size.x - (self.right_width + style.padding.x), self.position.y,
|
|
self.right_width + style.padding.x, self.size.y
|
|
)
|
|
for _, item in ipairs(self.active_items) do
|
|
local item_x = self.right_xoffset + item.x + style.padding.x
|
|
local hovered, item_bg = get_item_bg_color(self, item)
|
|
if item.alignment == StatusView.Item.RIGHT then
|
|
if type(item_bg) == "table" then
|
|
renderer.draw_rect(
|
|
item_x, self.position.y,
|
|
item.w, self.size.y, item_bg
|
|
)
|
|
end
|
|
if item.on_draw then
|
|
core.push_clip_rect(item_x, self.position.y, item.w, self.size.y)
|
|
item.on_draw(item_x, self.position.y, self.size.y, hovered)
|
|
core.pop_clip_rect()
|
|
else
|
|
self:draw_items(item.cached_item, false, item_x - style.padding.x)
|
|
end
|
|
end
|
|
end
|
|
core.pop_clip_rect()
|
|
|
|
-- draw tooltip
|
|
if self.hovered_item.tooltip ~= "" and self.hovered_item.active then
|
|
self:draw_item_tooltip(self.hovered_item)
|
|
end
|
|
end
|
|
end
|
|
|
|
if clicks > 5 then
|
|
if config.stonks == nil then clicks = -1 end
|
|
core.root_view:defer_draw(function()
|
|
local font = type(config.stonks) == "table" and config.stonks.font or style.icon_font
|
|
local icon = type(config.stonks) == "table" and config.stonks.icon or ( config.stonks and "g" or "h" )
|
|
local xadv = renderer.draw_text(font, icon, gx, gy, gc)
|
|
local x2, y2 = core.root_view.size.x - (xadv - gx), core.root_view.size.y - font:get_height()
|
|
gx, gy = common.clamp(gx + dx, 0, x2), common.clamp(gy + dy, 0, y2)
|
|
local odx, ody = dx, dy
|
|
if gx <= 0 then dx = math.abs(dx) elseif gx >= x2 then dx = -math.abs(dx) end
|
|
if gy <= 0 then dy = math.abs(dy) elseif gy >= y2 then dy = -math.abs(dy) end
|
|
if odx ~= dx or ody ~= dy then
|
|
local major = math.random(1, 3)
|
|
for i = 1, 3 do gc[i] = major == i and math.random(200, 255) or math.random(0, 100) end
|
|
end
|
|
core.redraw = true
|
|
end)
|
|
end
|
|
end
|
|
|
|
return StatusView
|