From 3eba7cd7f17781786de4b1a431bc8668a03e8fe5 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Fri, 17 Sep 2021 02:47:34 +0200 Subject: [PATCH] Implement tab drag and drop --- data/core/common.lua | 5 + data/core/rootview.lua | 272 +++++++++++++++++++++++++++++++++++++---- data/core/style.lua | 2 + 3 files changed, 256 insertions(+), 23 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index 9f3102bb..1a1b22cd 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -41,6 +41,11 @@ function common.lerp(a, b, t) end +function common.distance(x1, y1, x2, y2) + return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) +end + + function common.color(str) local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)") if r then diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 8a927077..bf438aa5 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -712,6 +712,62 @@ function Node:resize(axis, value) 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() @@ -719,6 +775,14 @@ function RootView:new() 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 @@ -802,10 +866,12 @@ function RootView:on_mouse_pressed(button, x, y, clicks) if button == "middle" or node.hovered_close == idx then node:close_view(self.root_node, node.views[idx]) else - self.dragged_node = { node, idx } + 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 - else + 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) @@ -814,14 +880,73 @@ function RootView:on_mouse_pressed(button, x, y, clicks) end -function RootView:on_mouse_released(...) +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 - self.dragged_node = nil + 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 - self.root_node:on_mouse_released(...) end @@ -857,6 +982,19 @@ function RootView:on_mouse_moved(x, y, dx, dy) 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) @@ -871,24 +1009,6 @@ function RootView:on_mouse_moved(x, y, dx, dy) elseif node then core.request_cursor(node.active_view.cursor) end - if node and self.dragged_node and (self.dragged_node[1] ~= node or (tab_index and self.dragged_node[2] ~= tab_index)) - and node.type == "leaf" and #node.views > 0 and node.views[1]:is(DocView) then - local tab = self.dragged_node[1].views[self.dragged_node[2]] - if self.dragged_node[1] ~= node then - for i, v in ipairs(node.views) do if v.doc == tab.doc then tab = nil break end end - if tab then - self.dragged_node[1]:remove_view(self.root_node, tab) - node:add_view(tab, tab_index) - self.root_node:update_layout() - self.dragged_node = { node, tab_index or #node.views } - core.redraw = true - end - else - table.remove(self.dragged_node[1].views, self.dragged_node[2]) - table.insert(node.views, tab_index, tab) - self.dragged_node = { node, tab_index } - end - end end @@ -909,10 +1029,110 @@ function RootView:on_focus_lost(...) 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 @@ -922,6 +1142,12 @@ function RootView:draw() 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 diff --git a/data/core/style.lua b/data/core/style.lua index faca166e..9a6efb50 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -44,6 +44,8 @@ style.scrollbar2 = { common.color "#4b4b52" } style.nagbar = { common.color "#FF0000" } style.nagbar_text = { common.color "#FFFFFF" } style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" } +style.drag_overlay = { common.color "rgba(255,255,255,0.1)" } +style.drag_overlay_tab = { common.color "#93DDFA" } style.syntax = {} style.syntax["normal"] = { common.color "#e1e1e6" }