local core = require "core" local common = require "core.common" local style = require "core.style" local Node = require "core.node" local View = require "core.view" local DocView = require "core.docview" ---@class core.rootview : core.view ---@field super core.view ---@field root_node core.node ---@field mouse core.view.position 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 ---@return core.node function RootView:get_active_node() local node = self.root_node:get_node_for_view(core.active_view) if not node then node = self:get_primary_node() end return node end ---@return core.node 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 ---@return core.node function RootView:get_active_node_default() local node = self.root_node:get_node_for_view(core.active_view) if not node then node = self:get_primary_node() end 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 ---@return core.node function RootView:get_primary_node() return get_primary_node(self.root_node) end ---@param node core.node ---@return core.node local function select_next_primary_node(node) if node.is_primary_node then return end if node.type ~= "leaf" then return select_next_primary_node(node.a) or select_next_primary_node(node.b) else local lx, ly = node:get_locked_size() if not lx and not ly then return node end end end ---@return core.node function RootView:select_next_primary_node() return select_next_primary_node(self.root_node) end ---@param doc core.doc ---@return core.docview 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 ---@param keep_active boolean 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. ---@param button core.view.mousebutton ---@param x number ---@param y number ---@param clicks integer function RootView.on_view_mouse_pressed(button, x, y, clicks) end ---@param button core.view.mousebutton ---@param x number ---@param y number ---@param clicks integer ---@return boolean function RootView:on_mouse_pressed(button, x, y, clicks) local div = self.root_node:get_divider_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y) if div and (node and not node.active_view:scrollbar_overlaps_point(x, y)) then self.dragged_divider = div return true end if node.hovered_scroll_button > 0 then node:scroll_tabs(node.hovered_scroll_button) return true 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]) return true 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]) return true end elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs core.set_active_view(node.active_view) return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks) 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 ---@param button core.view.mousebutton ---@param x number ---@param y number 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 ---@param x number ---@param y number ---@param dx number ---@param dy number 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 last_overlapping_node = self.overlapping_node self.overlapping_node = self.root_node:get_child_overlapping_point(x, y) if last_overlapping_node and last_overlapping_node ~= self.overlapping_node then last_overlapping_node:on_mouse_left() end local div = self.root_node:get_divider_overlapping_point(x, y) local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y) if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then core.request_cursor("arrow") elseif div and (self.overlapping_node and not self.overlapping_node.active_view:scrollbar_overlaps_point(x, y)) then core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") elseif tab_index then core.request_cursor("arrow") elseif self.overlapping_node then core.request_cursor(self.overlapping_node.active_view.cursor) end end function RootView:on_mouse_left() if self.overlapping_node then self.overlapping_node:on_mouse_left() end end ---@param filename string ---@param x number ---@param y number ---@return boolean function RootView:on_file_dropped(filename, x, y) local node = self.root_node:get_child_overlapping_point(x, y) return node and node.active_view:on_file_dropped(filename, x, y) 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) return 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, nil, "tab_drag") self:move_towards(overlay, "y", overlay.to.y, nil, "tab_drag") self:move_towards(overlay, "w", overlay.to.w, nil, "tab_drag") self:move_towards(overlay, "h", overlay.to.h, nil, "tab_drag") self:move_towards(overlay, "opacity", overlay.visible and 100 or 0, nil, "tab_drag") overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100 end function RootView:update() Node.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 view = dn.node.views[dn.idx] self.root_node:draw_tab(view, 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