The last column of pixel on the window's right side isn't correctly drawn and pixels appear dirty and more noticeably when the a NagView message was previously shown, a stripe of red pixels remains on the right. We use now a more souding roundig scheme. Now the rectangles to clip or to draw are passed around as Lua numbers without any rounding. In turns, when the rect coordinates are passed to the renderer we ensure the border of the rect are correctly snapped to the pixel's grid. It works by computing the coordinates of the edges, round them to integers and then compute the rect's width based on the rounded coordinates values.
1162 lines
35 KiB
Lua
1162 lines
35 KiB
Lua
local core = require "core"
|
|
local common = require "core.common"
|
|
local config = require "core.config"
|
|
local style = require "core.style"
|
|
local keymap = require "core.keymap"
|
|
local Object = require "core.object"
|
|
local View = require "core.view"
|
|
local NagView = require "core.nagview"
|
|
local DocView = require "core.docview"
|
|
|
|
|
|
local EmptyView = View:extend()
|
|
|
|
local function draw_text(x, y, color)
|
|
local th = style.big_font:get_height()
|
|
local dh = 2 * th + style.padding.y * 2
|
|
local x1, y1 = x, y + (dh - th) / 2
|
|
x = renderer.draw_text(style.big_font, "Lite XL", x1, y1, color)
|
|
renderer.draw_text(style.font, "version " .. VERSION, x1, y1 + th, color)
|
|
x = x + style.padding.x
|
|
renderer.draw_rect(x, y, math.ceil(1 * SCALE), dh, color)
|
|
local lines = {
|
|
{ fmt = "%s to run a command", cmd = "core:find-command" },
|
|
{ fmt = "%s to open a file from the project", cmd = "core:find-file" },
|
|
{ fmt = "%s to change project folder", cmd = "core:change-project-folder" },
|
|
{ fmt = "%s to open a project folder", cmd = "core:open-project-folder" },
|
|
}
|
|
th = style.font:get_height()
|
|
y = y + (dh - (th + style.padding.y) * #lines) / 2
|
|
local w = 0
|
|
for _, line in ipairs(lines) do
|
|
local text = string.format(line.fmt, keymap.get_binding(line.cmd))
|
|
w = math.max(w, renderer.draw_text(style.font, text, x + style.padding.x, y, color))
|
|
y = y + th + style.padding.y
|
|
end
|
|
return w, dh
|
|
end
|
|
|
|
function EmptyView:draw()
|
|
self:draw_background(style.background)
|
|
local w, h = draw_text(0, 0, { 0, 0, 0, 0 })
|
|
local x = self.position.x + math.max(style.padding.x, (self.size.x - w) / 2)
|
|
local y = self.position.y + (self.size.y - h) / 2
|
|
draw_text(x, y, style.dim)
|
|
end
|
|
|
|
|
|
|
|
local Node = Object:extend()
|
|
|
|
function Node:new(type)
|
|
self.type = type or "leaf"
|
|
self.position = { x = 0, y = 0 }
|
|
self.size = { x = 0, y = 0 }
|
|
self.views = {}
|
|
self.divider = 0.5
|
|
if self.type == "leaf" then
|
|
self:add_view(EmptyView())
|
|
end
|
|
self.hovered = {x = -1, y = -1 }
|
|
self.hovered_close = 0
|
|
self.tab_shift = 0
|
|
self.tab_offset = 1
|
|
self.tab_width = style.tab_width
|
|
self.move_towards = View.move_towards
|
|
end
|
|
|
|
|
|
function Node:propagate(fn, ...)
|
|
self.a[fn](self.a, ...)
|
|
self.b[fn](self.b, ...)
|
|
end
|
|
|
|
|
|
function Node:on_mouse_moved(x, y, ...)
|
|
if self.type == "leaf" then
|
|
self.hovered.x, self.hovered.y = x, y
|
|
self.active_view:on_mouse_moved(x, y, ...)
|
|
else
|
|
self:propagate("on_mouse_moved", x, y, ...)
|
|
end
|
|
end
|
|
|
|
|
|
function Node:on_mouse_released(...)
|
|
if self.type == "leaf" then
|
|
self.active_view:on_mouse_released(...)
|
|
else
|
|
self:propagate("on_mouse_released", ...)
|
|
end
|
|
end
|
|
|
|
|
|
function Node:consume(node)
|
|
for k, _ in pairs(self) do self[k] = nil end
|
|
for k, v in pairs(node) do self[k] = v end
|
|
end
|
|
|
|
|
|
local type_map = { up="vsplit", down="vsplit", left="hsplit", right="hsplit" }
|
|
|
|
-- The "locked" argument below should be in the form {x = <boolean>, y = <boolean>}
|
|
-- and it indicates if the node want to have a fixed size along the axis where the
|
|
-- boolean is true. If not it will be expanded to take all the available space.
|
|
-- The "resizable" flag indicates if, along the "locked" axis the node can be resized
|
|
-- by the user. If the node is marked as resizable their view should provide a
|
|
-- set_target_size method.
|
|
function Node:split(dir, view, locked, resizable)
|
|
assert(self.type == "leaf", "Tried to split non-leaf node")
|
|
local node_type = assert(type_map[dir], "Invalid direction")
|
|
local last_active = core.active_view
|
|
local child = Node()
|
|
child:consume(self)
|
|
self:consume(Node(node_type))
|
|
self.a = child
|
|
self.b = Node()
|
|
if view then self.b:add_view(view) end
|
|
if locked then
|
|
assert(type(locked) == 'table')
|
|
self.b.locked = locked
|
|
self.b.resizable = resizable or false
|
|
core.set_active_view(last_active)
|
|
end
|
|
if dir == "up" or dir == "left" then
|
|
self.a, self.b = self.b, self.a
|
|
return self.a
|
|
end
|
|
return self.b
|
|
end
|
|
|
|
function Node:remove_view(root, view)
|
|
if #self.views > 1 then
|
|
local idx = self:get_view_idx(view)
|
|
if idx < self.tab_offset then
|
|
self.tab_offset = self.tab_offset - 1
|
|
end
|
|
table.remove(self.views, idx)
|
|
if self.active_view == view then
|
|
self:set_active_view(self.views[idx] or self.views[#self.views])
|
|
end
|
|
else
|
|
local parent = self:get_parent_node(root)
|
|
local is_a = (parent.a == self)
|
|
local other = parent[is_a and "b" or "a"]
|
|
local locked_size_x, locked_size_y = other:get_locked_size()
|
|
local locked_size
|
|
if parent.type == "hsplit" then
|
|
locked_size = locked_size_x
|
|
else
|
|
locked_size = locked_size_y
|
|
end
|
|
if self.is_primary_node or locked_size then
|
|
self.views = {}
|
|
self:add_view(EmptyView())
|
|
else
|
|
parent:consume(other)
|
|
local p = parent
|
|
while p.type ~= "leaf" do
|
|
p = p[is_a and "a" or "b"]
|
|
end
|
|
p:set_active_view(p.active_view)
|
|
if self.is_primary_node then
|
|
p.is_primary_node = true
|
|
end
|
|
end
|
|
end
|
|
core.last_active_view = nil
|
|
end
|
|
|
|
function Node:close_view(root, view)
|
|
local do_close = function()
|
|
self:remove_view(root, view)
|
|
end
|
|
view:try_close(do_close)
|
|
end
|
|
|
|
|
|
function Node:close_active_view(root)
|
|
self:close_view(root, self.active_view)
|
|
end
|
|
|
|
|
|
function Node:add_view(view, idx)
|
|
assert(self.type == "leaf", "Tried to add view to non-leaf node")
|
|
assert(not self.locked, "Tried to add view to locked node")
|
|
if self.views[1] and self.views[1]:is(EmptyView) then
|
|
table.remove(self.views)
|
|
end
|
|
table.insert(self.views, idx or (#self.views + 1), view)
|
|
self:set_active_view(view)
|
|
end
|
|
|
|
|
|
function Node:set_active_view(view)
|
|
assert(self.type == "leaf", "Tried to set active view on non-leaf node")
|
|
self.active_view = view
|
|
core.set_active_view(view)
|
|
end
|
|
|
|
|
|
function Node:get_view_idx(view)
|
|
for i, v in ipairs(self.views) do
|
|
if v == view then return i end
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_node_for_view(view)
|
|
for _, v in ipairs(self.views) do
|
|
if v == view then return self end
|
|
end
|
|
if self.type ~= "leaf" then
|
|
return self.a:get_node_for_view(view) or self.b:get_node_for_view(view)
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_parent_node(root)
|
|
if root.a == self or root.b == self then
|
|
return root
|
|
elseif root.type ~= "leaf" then
|
|
return self:get_parent_node(root.a) or self:get_parent_node(root.b)
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_children(t)
|
|
t = t or {}
|
|
for _, view in ipairs(self.views) do
|
|
table.insert(t, view)
|
|
end
|
|
if self.a then self.a:get_children(t) end
|
|
if self.b then self.b:get_children(t) end
|
|
return t
|
|
end
|
|
|
|
|
|
-- return the width including the padding space and separately
|
|
-- the padding space itself
|
|
local function get_scroll_button_width()
|
|
local w = style.icon_font:get_width(">")
|
|
local pad = w
|
|
return w + 2 * pad, pad
|
|
end
|
|
|
|
|
|
function Node:get_divider_overlapping_point(px, py)
|
|
if self.type ~= "leaf" then
|
|
local axis = self.type == "hsplit" and "x" or "y"
|
|
if self.a:is_resizable(axis) and self.b:is_resizable(axis) then
|
|
local p = 6
|
|
local x, y, w, h = self:get_divider_rect()
|
|
x, y = x - p, y - p
|
|
w, h = w + p * 2, h + p * 2
|
|
if px > x and py > y and px < x + w and py < y + h then
|
|
return self
|
|
end
|
|
end
|
|
return self.a:get_divider_overlapping_point(px, py)
|
|
or self.b:get_divider_overlapping_point(px, py)
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_visible_tabs_number()
|
|
return math.min(#self.views - self.tab_offset + 1, config.max_tabs)
|
|
end
|
|
|
|
|
|
function Node:get_tab_overlapping_point(px, py)
|
|
if not self:should_show_tabs() then return nil end
|
|
local tabs_number = self:get_visible_tabs_number()
|
|
local x1, y1, w, h = self:get_tab_rect(self.tab_offset)
|
|
local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number)
|
|
if px >= x1 and py >= y1 and px < x2 and py < y1 + h then
|
|
return math.floor((px - x1) / w) + self.tab_offset
|
|
end
|
|
end
|
|
|
|
|
|
function Node:should_show_tabs()
|
|
if self.locked then return false end
|
|
local dn = core.root_view.dragged_node
|
|
if #self.views > 1
|
|
or (dn and dn.dragging) then -- show tabs while dragging
|
|
return true
|
|
elseif config.always_show_tabs then
|
|
return not self.views[1]:is(EmptyView)
|
|
end
|
|
return false
|
|
end
|
|
|
|
|
|
local function close_button_location(x, w)
|
|
local cw = style.icon_font:get_width("C")
|
|
local pad = style.padding.y
|
|
return x + w - pad - cw, cw, pad
|
|
end
|
|
|
|
|
|
function Node:get_scroll_button_index(px, py)
|
|
if #self.views == 1 then return end
|
|
for i = 1, 2 do
|
|
local x, y, w, h = self:get_scroll_button_rect(i)
|
|
if px >= x and px < x + w and py >= y and py < y + h then
|
|
return i
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function Node:tab_hovered_update(px, py)
|
|
local tab_index = self:get_tab_overlapping_point(px, py)
|
|
self.hovered_tab = tab_index
|
|
self.hovered_close = 0
|
|
self.hovered_scroll_button = 0
|
|
if tab_index then
|
|
local x, y, w, h = self:get_tab_rect(tab_index)
|
|
local cx, cw = close_button_location(x, w)
|
|
if px >= cx and px < cx + cw and py >= y and py < y + h and config.tab_close_button then
|
|
self.hovered_close = tab_index
|
|
end
|
|
else
|
|
self.hovered_scroll_button = self:get_scroll_button_index(px, py) or 0
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_child_overlapping_point(x, y)
|
|
local child
|
|
if self.type == "leaf" then
|
|
return self
|
|
elseif self.type == "hsplit" then
|
|
child = (x < self.b.position.x) and self.a or self.b
|
|
elseif self.type == "vsplit" then
|
|
child = (y < self.b.position.y) and self.a or self.b
|
|
end
|
|
return child:get_child_overlapping_point(x, y)
|
|
end
|
|
|
|
|
|
function Node:get_scroll_button_rect(index)
|
|
local w, pad = get_scroll_button_width()
|
|
local h = style.font:get_height() + style.padding.y * 2
|
|
local x = self.position.x + (index == 1 and 0 or self.size.x - w)
|
|
return x, self.position.y, w, h, pad
|
|
end
|
|
|
|
|
|
function Node:get_tab_rect(idx)
|
|
local sbw = get_scroll_button_width()
|
|
local maxw = self.size.x - 2 * sbw
|
|
local x0 = self.position.x + sbw
|
|
local x1 = x0 + common.clamp(self.tab_width * (idx - 1) - self.tab_shift, 0, maxw)
|
|
local x2 = x0 + common.clamp(self.tab_width * idx - self.tab_shift, 0, maxw)
|
|
local h = style.font:get_height() + style.padding.y * 2
|
|
return x1, self.position.y, x2 - x1, h
|
|
end
|
|
|
|
|
|
function Node:get_divider_rect()
|
|
local x, y = self.position.x, self.position.y
|
|
if self.type == "hsplit" then
|
|
return x + self.a.size.x, y, style.divider_size, self.size.y
|
|
elseif self.type == "vsplit" then
|
|
return x, y + self.a.size.y, self.size.x, style.divider_size
|
|
end
|
|
end
|
|
|
|
|
|
-- Return two values for x and y axis and each of them is either falsy or a number.
|
|
-- A falsy value indicate no fixed size along the corresponding direction.
|
|
function Node:get_locked_size()
|
|
if self.type == "leaf" then
|
|
if self.locked then
|
|
local size = self.active_view.size
|
|
-- The values below should be either a falsy value or a number
|
|
local sx = (self.locked and self.locked.x) and size.x
|
|
local sy = (self.locked and self.locked.y) and size.y
|
|
return sx, sy
|
|
end
|
|
else
|
|
local x1, y1 = self.a:get_locked_size()
|
|
local x2, y2 = self.b:get_locked_size()
|
|
-- The values below should be either a falsy value or a number
|
|
local sx, sy
|
|
if self.type == 'hsplit' then
|
|
if x1 and x2 then
|
|
local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size
|
|
sx = x1 + x2 + dsx
|
|
end
|
|
sy = y1 or y2
|
|
else
|
|
if y1 and y2 then
|
|
local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size
|
|
sy = y1 + y2 + dsy
|
|
end
|
|
sx = x1 or x2
|
|
end
|
|
return sx, sy
|
|
end
|
|
end
|
|
|
|
|
|
local function copy_position_and_size(dst, src)
|
|
dst.position.x, dst.position.y = src.position.x, src.position.y
|
|
dst.size.x, dst.size.y = src.size.x, src.size.y
|
|
end
|
|
|
|
|
|
-- calculating the sizes is the same for hsplits and vsplits, except the x/y
|
|
-- axis are swapped; this function lets us use the same code for both
|
|
local function calc_split_sizes(self, x, y, x1, x2, y1, y2)
|
|
local n
|
|
local ds = ((x1 and x1 < 1) or (x2 and x2 < 1)) and 0 or style.divider_size
|
|
if x1 then
|
|
n = x1 + ds
|
|
elseif x2 then
|
|
n = self.size[x] - x2
|
|
else
|
|
n = math.floor(self.size[x] * self.divider)
|
|
end
|
|
self.a.position[x] = self.position[x]
|
|
self.a.position[y] = self.position[y]
|
|
self.a.size[x] = n - ds
|
|
self.a.size[y] = self.size[y]
|
|
self.b.position[x] = self.position[x] + n
|
|
self.b.position[y] = self.position[y]
|
|
self.b.size[x] = self.size[x] - n
|
|
self.b.size[y] = self.size[y]
|
|
end
|
|
|
|
|
|
function Node:update_layout()
|
|
if self.type == "leaf" then
|
|
local av = self.active_view
|
|
if self:should_show_tabs() then
|
|
local _, _, _, th = self:get_tab_rect(1)
|
|
av.position.x, av.position.y = self.position.x, self.position.y + th
|
|
av.size.x, av.size.y = self.size.x, self.size.y - th
|
|
else
|
|
copy_position_and_size(av, self)
|
|
end
|
|
else
|
|
local x1, y1 = self.a:get_locked_size()
|
|
local x2, y2 = self.b:get_locked_size()
|
|
if self.type == "hsplit" then
|
|
calc_split_sizes(self, "x", "y", x1, x2)
|
|
elseif self.type == "vsplit" then
|
|
calc_split_sizes(self, "y", "x", y1, y2)
|
|
end
|
|
self.a:update_layout()
|
|
self.b:update_layout()
|
|
end
|
|
end
|
|
|
|
|
|
function Node:scroll_tabs_to_visible()
|
|
local index = self:get_view_idx(self.active_view)
|
|
if index then
|
|
local tabs_number = self:get_visible_tabs_number()
|
|
if self.tab_offset > index then
|
|
self.tab_offset = index
|
|
elseif self.tab_offset + tabs_number - 1 < index then
|
|
self.tab_offset = index - tabs_number + 1
|
|
elseif tabs_number < config.max_tabs and self.tab_offset > 1 then
|
|
self.tab_offset = #self.views - config.max_tabs + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function Node:scroll_tabs(dir)
|
|
local view_index = self:get_view_idx(self.active_view)
|
|
if dir == 1 then
|
|
if self.tab_offset > 1 then
|
|
self.tab_offset = self.tab_offset - 1
|
|
local last_index = self.tab_offset + self:get_visible_tabs_number() - 1
|
|
if view_index > last_index then
|
|
self:set_active_view(self.views[last_index])
|
|
end
|
|
end
|
|
elseif dir == 2 then
|
|
local tabs_number = self:get_visible_tabs_number()
|
|
if self.tab_offset + tabs_number - 1 < #self.views then
|
|
self.tab_offset = self.tab_offset + 1
|
|
local view_index = self:get_view_idx(self.active_view)
|
|
if view_index < self.tab_offset then
|
|
self:set_active_view(self.views[self.tab_offset])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function Node:target_tab_width()
|
|
local n = self:get_visible_tabs_number()
|
|
local w = self.size.x - get_scroll_button_width() * 2
|
|
return common.clamp(style.tab_width, w / config.max_tabs, w / n)
|
|
end
|
|
|
|
|
|
function Node:update()
|
|
if self.type == "leaf" then
|
|
self:scroll_tabs_to_visible()
|
|
for _, view in ipairs(self.views) do
|
|
view:update()
|
|
end
|
|
self:tab_hovered_update(self.hovered.x, self.hovered.y)
|
|
local tab_width = self:target_tab_width()
|
|
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1))
|
|
self:move_towards("tab_width", tab_width)
|
|
else
|
|
self.a:update()
|
|
self.b:update()
|
|
end
|
|
end
|
|
|
|
function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone)
|
|
local ds = style.divider_size
|
|
local dots_width = style.font:get_width("…")
|
|
local color = style.dim
|
|
local padding_y = style.padding.y
|
|
renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim)
|
|
if standalone then
|
|
renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2)
|
|
end
|
|
if is_active then
|
|
color = style.text
|
|
renderer.draw_rect(x, y, w, h, style.background)
|
|
renderer.draw_rect(x + w, y, ds, h, style.divider)
|
|
renderer.draw_rect(x - ds, y, ds, h, style.divider)
|
|
end
|
|
local cx, cw, cspace = close_button_location(x, w)
|
|
local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button)
|
|
if show_close_button then
|
|
local close_style = is_close_hovered and style.text or style.dim
|
|
common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h)
|
|
end
|
|
if is_hovered then
|
|
color = style.text
|
|
end
|
|
local padx = style.padding.x
|
|
-- Normally we should substract "cspace" from text_avail_width and from the
|
|
-- clipping width. It is the padding space we give to the left and right of the
|
|
-- close button. However, since we are using dots to terminate filenames, we
|
|
-- choose to ignore "cspace" accepting that the text can possibly "touch" the
|
|
-- close button.
|
|
local text_avail_width = cx - x - padx
|
|
core.push_clip_rect(x, y, cx - x, h)
|
|
x, w = x + padx, w - padx * 2
|
|
local align = "center"
|
|
if style.font:get_width(text) > text_avail_width then
|
|
align = "left"
|
|
for i = 1, #text do
|
|
local reduced_text = text:sub(1, #text - i)
|
|
if style.font:get_width(reduced_text) + dots_width <= text_avail_width then
|
|
text = reduced_text .. "…"
|
|
break
|
|
end
|
|
end
|
|
end
|
|
common.draw_text(style.font, color, text, align, x, y, w, h)
|
|
core.pop_clip_rect()
|
|
end
|
|
|
|
function Node:draw_tabs()
|
|
local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1)
|
|
local ds = style.divider_size
|
|
local dots_width = style.font:get_width("…")
|
|
core.push_clip_rect(x, y, self.size.x, h)
|
|
renderer.draw_rect(x, y, self.size.x, h, style.background2)
|
|
renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider)
|
|
|
|
if self.tab_offset > 1 then
|
|
local button_style = self.hovered_scroll_button == 1 and style.text or style.dim
|
|
common.draw_text(style.icon_font, button_style, "<", nil, x + scroll_padding, y, 0, h)
|
|
end
|
|
|
|
local tabs_number = self:get_visible_tabs_number()
|
|
if #self.views > self.tab_offset + tabs_number - 1 then
|
|
local xrb, yrb, wrb = self:get_scroll_button_rect(2)
|
|
local button_style = self.hovered_scroll_button == 2 and style.text or style.dim
|
|
common.draw_text(style.icon_font, button_style, ">", nil, xrb + scroll_padding, yrb, 0, h)
|
|
end
|
|
|
|
for i = self.tab_offset, self.tab_offset + tabs_number - 1 do
|
|
local view = self.views[i]
|
|
local x, y, w, h = self:get_tab_rect(i)
|
|
self:draw_tab(view:get_name(), view == self.active_view,
|
|
i == self.hovered_tab, i == self.hovered_close,
|
|
x, y, w, h)
|
|
end
|
|
|
|
core.pop_clip_rect()
|
|
end
|
|
|
|
|
|
function Node:draw()
|
|
if self.type == "leaf" then
|
|
if self:should_show_tabs() then
|
|
self:draw_tabs()
|
|
end
|
|
local pos, size = self.active_view.position, self.active_view.size
|
|
core.push_clip_rect(pos.x, pos.y, pos.x + size.x, pos.y + size.y)
|
|
self.active_view:draw()
|
|
core.pop_clip_rect()
|
|
else
|
|
local x, y, w, h = self:get_divider_rect()
|
|
renderer.draw_rect(x, y, w, h, style.divider)
|
|
self:propagate("draw")
|
|
end
|
|
end
|
|
|
|
|
|
function Node:is_empty()
|
|
if self.type == "leaf" then
|
|
return #self.views == 0 or (#self.views == 1 and self.views[1]:is(EmptyView))
|
|
else
|
|
return self.a:is_empty() and self.b:is_empty()
|
|
end
|
|
end
|
|
|
|
|
|
function Node:close_all_docviews(keep_active)
|
|
local node_active_view = self.active_view
|
|
local lost_active_view = false
|
|
if self.type == "leaf" then
|
|
local i = 1
|
|
while i <= #self.views do
|
|
local view = self.views[i]
|
|
if view.context == "session" and (not keep_active or view ~= self.active_view) then
|
|
table.remove(self.views, i)
|
|
if view == node_active_view then
|
|
lost_active_view = true
|
|
end
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
self.tab_offset = 1
|
|
if #self.views == 0 and self.is_primary_node then
|
|
-- if we are not the primary view and we had the active view it doesn't
|
|
-- matter to reattribute the active view because, within the close_all_docviews
|
|
-- top call, the primary node will take the active view anyway.
|
|
-- Set the empty view and takes the active view.
|
|
self:add_view(EmptyView())
|
|
elseif #self.views > 0 and lost_active_view then
|
|
-- In practice we never get there but if a view remain we need
|
|
-- to reset the Node's active view.
|
|
self:set_active_view(self.views[1])
|
|
end
|
|
else
|
|
self.a:close_all_docviews(keep_active)
|
|
self.b:close_all_docviews(keep_active)
|
|
if self.a:is_empty() and not self.a.is_primary_node then
|
|
self:consume(self.b)
|
|
elseif self.b:is_empty() and not self.b.is_primary_node then
|
|
self:consume(self.a)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Returns true for nodes that accept either "proportional" resizes (based on the
|
|
-- node.divider) or "locked" resizable nodes (along the resize axis).
|
|
function Node:is_resizable(axis)
|
|
if self.type == 'leaf' then
|
|
return not self.locked or not self.locked[axis] or self.resizable
|
|
else
|
|
local a_resizable = self.a:is_resizable(axis)
|
|
local b_resizable = self.b:is_resizable(axis)
|
|
return a_resizable and b_resizable
|
|
end
|
|
end
|
|
|
|
|
|
-- Return true iff it is a locked pane along the rezise axis and is
|
|
-- declared "resizable".
|
|
function Node:is_locked_resizable(axis)
|
|
return self.locked and self.locked[axis] and self.resizable
|
|
end
|
|
|
|
|
|
function Node:resize(axis, value)
|
|
if self.type == 'leaf' then
|
|
-- If it is not locked we don't accept the
|
|
-- resize operation here because for proportional panes the resize is
|
|
-- done using the "divider" value of the parent node.
|
|
if self:is_locked_resizable(axis) then
|
|
return self.active_view:set_target_size(axis, value)
|
|
end
|
|
else
|
|
if self.type == (axis == "x" and "hsplit" or "vsplit") then
|
|
-- we are resizing a node that is splitted along the resize axis
|
|
if self.a:is_locked_resizable(axis) and self.b:is_locked_resizable(axis) then
|
|
local rem_value = value - self.a.size[axis]
|
|
if rem_value >= 0 then
|
|
return self.b.active_view:set_target_size(axis, rem_value)
|
|
else
|
|
self.b.active_view:set_target_size(axis, 0)
|
|
return self.a.active_view:set_target_size(axis, value)
|
|
end
|
|
end
|
|
else
|
|
-- we are resizing a node that is splitted along the axis perpendicular
|
|
-- to the resize axis
|
|
local a_resizable = self.a:is_resizable(axis)
|
|
local b_resizable = self.b:is_resizable(axis)
|
|
if a_resizable and b_resizable then
|
|
self.a:resize(axis, value)
|
|
self.b:resize(axis, value)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_split_type(mouse_x, mouse_y)
|
|
local x, y = self.position.x, self.position.y
|
|
local w, h = self.size.x, self.size.y
|
|
local _, _, _, tab_h = self:get_scroll_button_rect(1)
|
|
y = y + tab_h
|
|
h = h - tab_h
|
|
|
|
local local_mouse_x = mouse_x - x
|
|
local local_mouse_y = mouse_y - y
|
|
|
|
if local_mouse_y < 0 then
|
|
return "tab"
|
|
else
|
|
local left_pct = local_mouse_x * 100 / w
|
|
local top_pct = local_mouse_y * 100 / h
|
|
if left_pct <= 30 then
|
|
return "left"
|
|
elseif left_pct >= 70 then
|
|
return "right"
|
|
elseif top_pct <= 30 then
|
|
return "up"
|
|
elseif top_pct >= 70 then
|
|
return "down"
|
|
end
|
|
return "middle"
|
|
end
|
|
end
|
|
|
|
|
|
function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index)
|
|
local tab_index = self:get_tab_overlapping_point(x, y)
|
|
if not tab_index then
|
|
local first_tab_x = self:get_tab_rect(1)
|
|
if x < first_tab_x then
|
|
-- mouse before first visible tab
|
|
tab_index = self.tab_offset or 1
|
|
else
|
|
-- mouse after last visible tab
|
|
tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0)
|
|
end
|
|
end
|
|
local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index)
|
|
if x > tab_x + tab_w / 2 and tab_index <= #self.views then
|
|
-- use next tab
|
|
tab_x = tab_x + tab_w
|
|
tab_index = tab_index + 1
|
|
end
|
|
if self == dragged_node and dragged_index and tab_index > dragged_index then
|
|
-- the tab we are moving is counted in tab_index
|
|
tab_index = tab_index - 1
|
|
tab_x = tab_x - tab_w
|
|
end
|
|
return tab_index, tab_x, tab_y, tab_w, tab_h
|
|
end
|
|
|
|
|
|
local RootView = View:extend()
|
|
|
|
function RootView:new()
|
|
RootView.super.new(self)
|
|
self.root_node = Node()
|
|
self.deferred_draws = {}
|
|
self.mouse = { x = 0, y = 0 }
|
|
self.drag_overlay = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0,
|
|
base_color = style.drag_overlay,
|
|
color = { table.unpack(style.drag_overlay) } }
|
|
self.drag_overlay.to = { x = 0, y = 0, w = 0, h = 0 }
|
|
self.drag_overlay_tab = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0,
|
|
base_color = style.drag_overlay_tab,
|
|
color = { table.unpack(style.drag_overlay_tab) } }
|
|
self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 }
|
|
end
|
|
|
|
|
|
function RootView:defer_draw(fn, ...)
|
|
table.insert(self.deferred_draws, 1, { fn = fn, ... })
|
|
end
|
|
|
|
|
|
function RootView:get_active_node()
|
|
return self.root_node:get_node_for_view(core.active_view)
|
|
end
|
|
|
|
|
|
local function get_primary_node(node)
|
|
if node.is_primary_node then
|
|
return node
|
|
end
|
|
if node.type ~= "leaf" then
|
|
return get_primary_node(node.a) or get_primary_node(node.b)
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:get_active_node_default()
|
|
local node = self.root_node:get_node_for_view(core.active_view)
|
|
if node.locked then
|
|
local default_view = self:get_primary_node().views[1]
|
|
assert(default_view, "internal error: cannot find original document node.")
|
|
core.set_active_view(default_view)
|
|
node = self:get_active_node()
|
|
end
|
|
return node
|
|
end
|
|
|
|
|
|
function RootView:get_primary_node()
|
|
return get_primary_node(self.root_node)
|
|
end
|
|
|
|
|
|
function RootView:open_doc(doc)
|
|
local node = self:get_active_node_default()
|
|
for i, view in ipairs(node.views) do
|
|
if view.doc == doc then
|
|
node:set_active_view(node.views[i])
|
|
return view
|
|
end
|
|
end
|
|
local view = DocView(doc)
|
|
node:add_view(view)
|
|
self.root_node:update_layout()
|
|
view:scroll_to_line(view.doc:get_selection(), true, true)
|
|
return view
|
|
end
|
|
|
|
|
|
function RootView:close_all_docviews(keep_active)
|
|
self.root_node:close_all_docviews(keep_active)
|
|
end
|
|
|
|
|
|
-- Function to intercept mouse pressed events on the active view.
|
|
-- Do nothing by default.
|
|
function RootView.on_view_mouse_pressed(button, x, y, clicks)
|
|
end
|
|
|
|
|
|
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
|
|
if button == "left" then
|
|
self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y}
|
|
end
|
|
node:set_active_view(node.views[idx])
|
|
end
|
|
elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
|
|
core.set_active_view(node.active_view)
|
|
if not self.on_view_mouse_pressed(button, x, y, clicks) then
|
|
node.active_view:on_mouse_pressed(button, x, y, clicks)
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:get_overlay_base_color(overlay)
|
|
if overlay == self.drag_overlay then
|
|
return style.drag_overlay
|
|
else
|
|
return style.drag_overlay_tab
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:set_show_overlay(overlay, status)
|
|
overlay.visible = status
|
|
if status then -- reset colors
|
|
-- reload base_color
|
|
overlay.base_color = self:get_overlay_base_color(overlay)
|
|
overlay.color[1] = overlay.base_color[1]
|
|
overlay.color[2] = overlay.base_color[2]
|
|
overlay.color[3] = overlay.base_color[3]
|
|
overlay.color[4] = overlay.base_color[4]
|
|
overlay.opacity = 0
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:on_mouse_released(button, x, y, ...)
|
|
if self.dragged_divider then
|
|
self.dragged_divider = nil
|
|
end
|
|
if self.dragged_node then
|
|
if button == "left" then
|
|
if self.dragged_node.dragging then
|
|
local node = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y)
|
|
local dragged_node = self.dragged_node.node
|
|
|
|
if node and not node.locked
|
|
-- don't do anything if dragging onto own node, with only one view
|
|
and (node ~= dragged_node or #node.views > 1) then
|
|
local split_type = node:get_split_type(self.mouse.x, self.mouse.y)
|
|
local view = dragged_node.views[self.dragged_node.idx]
|
|
|
|
if split_type ~= "middle" and split_type ~= "tab" then -- needs splitting
|
|
local new_node = node:split(split_type)
|
|
self.root_node:get_node_for_view(view):remove_view(self.root_node, view)
|
|
new_node:add_view(view)
|
|
elseif split_type == "middle" and node ~= dragged_node then -- move to other node
|
|
dragged_node:remove_view(self.root_node, view)
|
|
node:add_view(view)
|
|
self.root_node:get_node_for_view(view):set_active_view(view)
|
|
elseif split_type == "tab" then -- move besides other tabs
|
|
local tab_index = node:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y, dragged_node, self.dragged_node.idx)
|
|
dragged_node:remove_view(self.root_node, view)
|
|
node:add_view(view, tab_index)
|
|
self.root_node:get_node_for_view(view):set_active_view(view)
|
|
end
|
|
self.root_node:update_layout()
|
|
core.redraw = true
|
|
end
|
|
end
|
|
self:set_show_overlay(self.drag_overlay, false)
|
|
self:set_show_overlay(self.drag_overlay_tab, false)
|
|
if self.dragged_node and self.dragged_node.dragging then
|
|
core.request_cursor("arrow")
|
|
end
|
|
self.dragged_node = nil
|
|
end
|
|
else -- avoid sending on_mouse_released events when dragging tabs
|
|
self.root_node:on_mouse_released(button, x, y, ...)
|
|
end
|
|
end
|
|
|
|
|
|
local function resize_child_node(node, axis, value, delta)
|
|
local accept_resize = node.a:resize(axis, value)
|
|
if not accept_resize then
|
|
accept_resize = node.b:resize(axis, node.size[axis] - value)
|
|
end
|
|
if not accept_resize then
|
|
node.divider = node.divider + delta / node.size[axis]
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:on_mouse_moved(x, y, dx, dy)
|
|
if core.active_view == core.nag_view then
|
|
core.request_cursor("arrow")
|
|
core.active_view:on_mouse_moved(x, y, dx, dy)
|
|
return
|
|
end
|
|
|
|
if self.dragged_divider then
|
|
local node = self.dragged_divider
|
|
if node.type == "hsplit" then
|
|
x = common.clamp(x, 0, self.root_node.size.x * 0.95)
|
|
resize_child_node(node, "x", x, dx)
|
|
elseif node.type == "vsplit" then
|
|
y = common.clamp(y, 0, self.root_node.size.y * 0.95)
|
|
resize_child_node(node, "y", y, dy)
|
|
end
|
|
node.divider = common.clamp(node.divider, 0.01, 0.99)
|
|
return
|
|
end
|
|
|
|
self.mouse.x, self.mouse.y = x, y
|
|
|
|
local dn = self.dragged_node
|
|
if dn and not dn.dragging then
|
|
-- start dragging only after enough movement
|
|
dn.dragging = common.distance(x, y, dn.drag_start_x, dn.drag_start_y) > style.tab_width * .05
|
|
if dn.dragging then
|
|
core.request_cursor("hand")
|
|
end
|
|
end
|
|
|
|
-- avoid sending on_mouse_moved events when dragging tabs
|
|
if dn then return end
|
|
|
|
self.root_node:on_mouse_moved(x, y, dx, dy)
|
|
|
|
local node = self.root_node:get_child_overlapping_point(x, y)
|
|
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
|
|
core.request_cursor("arrow")
|
|
elseif div then
|
|
core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
|
|
elseif tab_index then
|
|
core.request_cursor("arrow")
|
|
elseif node then
|
|
core.request_cursor(node.active_view.cursor)
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:on_mouse_wheel(...)
|
|
local x, y = self.mouse.x, self.mouse.y
|
|
local node = self.root_node:get_child_overlapping_point(x, y)
|
|
node.active_view:on_mouse_wheel(...)
|
|
end
|
|
|
|
|
|
function RootView:on_text_input(...)
|
|
core.active_view:on_text_input(...)
|
|
end
|
|
|
|
|
|
function RootView:on_focus_lost(...)
|
|
-- We force a redraw so documents can redraw without the cursor.
|
|
core.redraw = true
|
|
end
|
|
|
|
|
|
function RootView:interpolate_drag_overlay(overlay)
|
|
self:move_towards(overlay, "x", overlay.to.x)
|
|
self:move_towards(overlay, "y", overlay.to.y)
|
|
self:move_towards(overlay, "w", overlay.to.w)
|
|
self:move_towards(overlay, "h", overlay.to.h)
|
|
|
|
self:move_towards(overlay, "opacity", overlay.visible and 100 or 0)
|
|
overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100
|
|
end
|
|
|
|
|
|
function RootView:update()
|
|
copy_position_and_size(self.root_node, self)
|
|
self.root_node:update()
|
|
self.root_node:update_layout()
|
|
|
|
self:update_drag_overlay()
|
|
self:interpolate_drag_overlay(self.drag_overlay)
|
|
self:interpolate_drag_overlay(self.drag_overlay_tab)
|
|
end
|
|
|
|
|
|
function RootView:set_drag_overlay(overlay, x, y, w, h, immediate)
|
|
overlay.to.x = x
|
|
overlay.to.y = y
|
|
overlay.to.w = w
|
|
overlay.to.h = h
|
|
if immediate then
|
|
overlay.x = x
|
|
overlay.y = y
|
|
overlay.w = w
|
|
overlay.h = h
|
|
end
|
|
if not overlay.visible then
|
|
self:set_show_overlay(overlay, true)
|
|
end
|
|
end
|
|
|
|
|
|
local function get_split_sizes(split_type, x, y, w, h)
|
|
if split_type == "left" then
|
|
w = w * .5
|
|
elseif split_type == "right" then
|
|
x = x + w * .5
|
|
w = w * .5
|
|
elseif split_type == "up" then
|
|
h = h * .5
|
|
elseif split_type == "down" then
|
|
y = y + h * .5
|
|
h = h * .5
|
|
end
|
|
return x, y, w, h
|
|
end
|
|
|
|
|
|
function RootView:update_drag_overlay()
|
|
if not (self.dragged_node and self.dragged_node.dragging) then return end
|
|
local over = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y)
|
|
if over and not over.locked then
|
|
local _, _, _, tab_h = over:get_scroll_button_rect(1)
|
|
local x, y = over.position.x, over.position.y
|
|
local w, h = over.size.x, over.size.y
|
|
local split_type = over:get_split_type(self.mouse.x, self.mouse.y)
|
|
|
|
if split_type == "tab" and (over ~= self.dragged_node.node or #over.views > 1) then
|
|
local tab_index, tab_x, tab_y, tab_w, tab_h = over:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y)
|
|
self:set_drag_overlay(self.drag_overlay_tab,
|
|
tab_x + (tab_index and 0 or tab_w), tab_y,
|
|
style.caret_width, tab_h,
|
|
-- avoid showing tab overlay moving between nodes
|
|
over ~= self.drag_overlay_tab.last_over)
|
|
self:set_show_overlay(self.drag_overlay, false)
|
|
self.drag_overlay_tab.last_over = over
|
|
else
|
|
if (over ~= self.dragged_node.node or #over.views > 1) then
|
|
y = y + tab_h
|
|
h = h - tab_h
|
|
x, y, w, h = get_split_sizes(split_type, x, y, w, h)
|
|
end
|
|
self:set_drag_overlay(self.drag_overlay, x, y, w, h)
|
|
self:set_show_overlay(self.drag_overlay_tab, false)
|
|
end
|
|
else
|
|
self:set_show_overlay(self.drag_overlay, false)
|
|
self:set_show_overlay(self.drag_overlay_tab, false)
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:draw_grabbed_tab()
|
|
local dn = self.dragged_node
|
|
local _,_, w, h = dn.node:get_tab_rect(dn.idx)
|
|
local x = self.mouse.x - w / 2
|
|
local y = self.mouse.y - h / 2
|
|
local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or ""
|
|
self.root_node:draw_tab(text, true, true, false, x, y, w, h, true)
|
|
end
|
|
|
|
|
|
function RootView:draw_drag_overlay(ov)
|
|
if ov.opacity > 0 then
|
|
renderer.draw_rect(ov.x, ov.y, ov.w, ov.h, ov.color)
|
|
end
|
|
end
|
|
|
|
|
|
function RootView:draw()
|
|
self.root_node:draw()
|
|
while #self.deferred_draws > 0 do
|
|
local t = table.remove(self.deferred_draws)
|
|
t.fn(table.unpack(t))
|
|
end
|
|
|
|
self:draw_drag_overlay(self.drag_overlay)
|
|
self:draw_drag_overlay(self.drag_overlay_tab)
|
|
if self.dragged_node and self.dragged_node.dragging then
|
|
self:draw_grabbed_tab()
|
|
end
|
|
if core.cursor_change_req then
|
|
system.set_cursor(core.cursor_change_req)
|
|
core.cursor_change_req = nil
|
|
end
|
|
end
|
|
|
|
|
|
return RootView
|