From 6a4cb29f8644a2d1cbc56fbd4e8433cc41420cc8 Mon Sep 17 00:00:00 2001 From: tacf Date: Sun, 16 Aug 2026 22:34:14 +0100 Subject: [PATCH 01/12] feat: abstract tree struct and use for rootview splitting --- data/core/commands/root.lua | 28 ++-- data/core/doc/init.lua | 2 +- data/core/rootview.lua | 215 ++++++++++++------------------- data/core/{ => utils}/object.lua | 0 data/core/utils/tree.lua | 68 ++++++++++ data/core/view.lua | 2 +- 6 files changed, 165 insertions(+), 150 deletions(-) rename data/core/{ => utils}/object.lua (100%) create mode 100644 data/core/utils/tree.lua diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 4223b16..3e37282 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -15,24 +15,24 @@ local t = { local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) idx = idx - 1 - if idx < 1 then idx = #node.views end - node:set_active_view(node.views[idx]) + if idx < 1 then idx = #node.nodes end + node:set_active_view(node.nodes[idx]) end, ["root:switch-to-next-tab"] = function() local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) idx = idx + 1 - if idx > #node.views then idx = 1 end - node:set_active_view(node.views[idx]) + if idx > #node.nodes then idx = 1 end + node:set_active_view(node.nodes[idx]) end, ["root:move-tab-left"] = function() local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) if idx > 1 then - table.remove(node.views, idx) - table.insert(node.views, idx - 1, core.active_view) + table.remove(node.nodes, idx) + table.insert(node.nodes, idx - 1, core.active_view) node:scroll_tab_into_view(core.active_view) end end, @@ -40,24 +40,24 @@ local t = { ["root:move-tab-right"] = function() local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) - if idx < #node.views then - table.remove(node.views, idx) - table.insert(node.views, idx + 1, core.active_view) + if idx < #node.nodes then + table.remove(node.nodes, idx) + table.insert(node.nodes, idx + 1, core.active_view) node:scroll_tab_into_view(core.active_view) end end, ["root:shrink"] = function() local node = core.root_view:get_active_node() - local parent = node:get_parent_node(core.root_view.root_node) - local n = (parent.a == node) and -0.1 or 0.1 + local parent = node:get_parent(core.root_view.root_node) + local n = (parent.nodes[node.LEFT] == node) and -0.1 or 0.1 parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) end, ["root:grow"] = function() local node = core.root_view:get_active_node() - local parent = node:get_parent_node(core.root_view.root_node) - local n = (parent.a == node) and 0.1 or -0.1 + local parent = node:get_parent(core.root_view.root_node) + local n = (parent.nodes[node.LEFT] == node) and 0.1 or -0.1 parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) end, } @@ -66,7 +66,7 @@ local t = { for i = 1, 9 do t["root:switch-to-tab-" .. i] = function() local node = core.root_view:get_active_node() - local view = node.views[i] + local view = node.nodes[i] if view then node:set_active_view(view) end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 7462cf2..b03d95b 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -1,4 +1,4 @@ -local Object = require "core.object" +local Object = require "core.utils.object" local syntax = require "core.syntax" local config = require "core.config" diff --git a/data/core/rootview.lua b/data/core/rootview.lua index b8de4a8..e7a30bd 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -2,7 +2,7 @@ local core = require "core" local common = require "core.common" local style = require "core.style" local keymap = require "core.keymap" -local Object = require "core.object" +local Tree = require "core.utils.tree" local View = require "core.view" local DocView = require "core.docview" @@ -16,7 +16,7 @@ local function draw_text(x, y, 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 run a command", cmd = "core:find-command" }, { fmt = "%s to open a file from the project", cmd = "core:find-file" }, } th = style.font:get_height() @@ -38,15 +38,16 @@ function EmptyView:draw() draw_text(x, y, style.dim) end +local Node = Tree:extend() - -local Node = Object:extend() +Node.LEFT = 1 +Node.RIGHT = 2 function Node:new(type) self.type = type or "leaf" self.position = { x = 0, y = 0 } self.size = { x = 0, y = 0 } - self.views = {} + self.nodes = {} self.divider = 0.5 self.tab_scroll = 0 if self.type == "leaf" then @@ -54,13 +55,6 @@ function Node:new(type) end end - -function Node:propagate(fn, ...) - self.a[fn](self.a, ...) - self.b[fn](self.b, ...) -end - - function Node:on_mouse_moved(x, y, ...) self.hovered_tab = self:get_tab_overlapping_point(x, y) self.hovered_tab_close = self:get_tab_close_overlapping_point(x, y) @@ -71,7 +65,6 @@ function Node:on_mouse_moved(x, y, ...) end end - function Node:on_mouse_released(...) if self.type == "leaf" then self.active_view:on_mouse_released(...) @@ -80,14 +73,7 @@ function Node: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" } +local type_map = { up = "vsplit", down = "vsplit", left = "hsplit", right = "hsplit" } -- locked == true: single-view pane sized by the view itself (not node.divider), @@ -100,43 +86,47 @@ function Node:split(dir, view, locked) local child = Node() child:consume(self) self:consume(Node(type)) - self.a = child - self.b = Node() - if view then self.b:add_view(view) end + local other = Node() + if view then other:add_view(view) end if locked then - self.b.locked = locked + other.locked = locked core.set_active_view(last_active) end + self.nodes = {} if dir == "up" or dir == "left" then - self.a, self.b = self.b, self.a + self.nodes[Node.LEFT] = other + self.nodes[Node.RIGHT] = child + else + self.nodes[Node.LEFT] = child + self.nodes[Node.RIGHT] = other end return child end - function Node:close_view(root, view) local do_close = function() local idx = self:get_view_idx(view) if not idx then return end - if #self.views > 1 then - table.remove(self.views, idx) + if #self.nodes > 1 then + table.remove(self.nodes, idx) if view == self.active_view then - self:set_active_view(self.views[idx] or self.views[#self.views]) + self:set_active_view(self.nodes[idx] or self.nodes[#self.nodes]) else self:scroll_tab_into_view(self.active_view) 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 parent = self:get_parent(root) + local side = (parent.nodes[Node.LEFT] == self) and Node.LEFT or Node.RIGHT + local other_side = (side == Node.LEFT) and Node.RIGHT or Node.LEFT + local other = parent.nodes[other_side] if other:get_locked_size() then - self.views = {} + self.nodes = {} self:add_view(EmptyView()) else parent:consume(other) local p = parent while p.type ~= "leaf" do - p = p[is_a and "a" or "b"] + p = p.nodes[side] end p:set_active_view(p.active_view) end @@ -146,23 +136,20 @@ function Node:close_view(root, view) 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) 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) + if self.nodes[1] and self.nodes[1]:is(EmptyView) then + table.remove(self.nodes) end - table.insert(self.views, view) + table.insert(self.nodes, 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 @@ -170,44 +157,36 @@ function Node:set_active_view(view) core.set_active_view(view) end - function Node:get_view_idx(view) - for i, v in ipairs(self.views) do + for i, v in ipairs(self.nodes) 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) + if self.type == "leaf" then + for _, v in ipairs(self.nodes) do + if v == view then return self end + end + else + return self.nodes[Node.LEFT]:get_node_for_view(view) or self.nodes[Node.RIGHT]:get_node_for_view(view) end end - function Node:get_children(t) t = t or {} - for _, view in ipairs(self.views) do - table.insert(t, view) + if self.type == "leaf" then + for _, view in ipairs(self.nodes) do + table.insert(t, view) + end + else + for _, node in ipairs(self.nodes) do + node:get_children(t) + end end - if self.a then self.a:get_children(t) end - if self.b then self.b:get_children(t) end return t end - function Node:get_divider_overlapping_point(px, py) if self.type ~= "leaf" then local p = 6 @@ -217,15 +196,14 @@ function Node:get_divider_overlapping_point(px, py) if px > x and py > y and px < x + w and py < y + h then return self end - return self.a:get_divider_overlapping_point(px, py) - or self.b:get_divider_overlapping_point(px, py) + return self.nodes[Node.LEFT]:get_divider_overlapping_point(px, py) + or self.nodes[Node.RIGHT]:get_divider_overlapping_point(px, py) end end - function Node:get_tab_metrics() local h = style.font:get_height() + style.padding.y * 2 - local view_count = #self.views + local view_count = #self.nodes local tab_width = style.tab_width local viewport_x = self.position.x local viewport_w = self.size.x @@ -235,24 +213,21 @@ function Node:get_tab_metrics() return tab_width, h, overflow, viewport_x, viewport_w, max_scroll end - function Node:get_tab_bar_overlapping_point(px, py) - if #self.views <= 1 then return false end + if #self.nodes <= 1 then return false end local _, h = self:get_tab_metrics() return px >= self.position.x and px < self.position.x + self.size.x - and py >= self.position.y and py < self.position.y + h + and py >= self.position.y and py < self.position.y + h end - function Node:get_tab_overlapping_point(px, py) if not self:get_tab_bar_overlapping_point(px, py) then return nil end local tw, _, _, viewport_x, viewport_w = self:get_tab_metrics() if px < viewport_x or px >= viewport_x + viewport_w then return nil end local idx = math.floor((px - viewport_x + self.tab_scroll) / tw) + 1 - return idx <= #self.views and idx or nil + return idx <= #self.nodes and idx or nil end - function Node:get_tab_close_overlapping_point(px, py) local idx = self:get_tab_overlapping_point(px, py) if not idx then return nil end @@ -260,7 +235,6 @@ function Node:get_tab_close_overlapping_point(px, py) if px >= x + w - h then return idx end end - function Node:scroll_tabs(direction) local tw, _, overflow, _, _, max_scroll = self:get_tab_metrics() if overflow then @@ -268,7 +242,6 @@ function Node:scroll_tabs(direction) end end - function Node:scroll_tab_into_view(view) local idx = self:get_view_idx(view) if not idx then return end @@ -287,36 +260,32 @@ function Node:scroll_tab_into_view(view) self.tab_scroll = common.clamp(self.tab_scroll, 0, max_scroll) 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 + child = (x < self.nodes[Node.RIGHT].position.x) and self.nodes[Node.LEFT] or self.nodes[Node.RIGHT] elseif self.type == "vsplit" then - child = (y < self.b.position.y) and self.a or self.b + child = (y < self.nodes[Node.RIGHT].position.y) and self.nodes[Node.LEFT] or self.nodes[Node.RIGHT] end return child:get_child_overlapping_point(x, y) end - function Node:get_tab_rect(idx) local tw, h, _, viewport_x = self:get_tab_metrics() - return viewport_x + (idx-1) * tw - self.tab_scroll, self.position.y, tw, h + return viewport_x + (idx - 1) * tw - self.tab_scroll, self.position.y, tw, 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 + return x + self.nodes[Node.LEFT].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 + return x, y + self.nodes[Node.LEFT].size.y, self.size.x, style.divider_size end end - function Node:get_locked_size() if self.type == "leaf" then if self.locked then @@ -324,8 +293,8 @@ function Node:get_locked_size() return size.x, size.y end else - local x1, y1 = self.a:get_locked_size() - local x2, y2 = self.b:get_locked_size() + local x1, y1 = self.nodes[Node.LEFT]:get_locked_size() + local x2, y2 = self.nodes[Node.RIGHT]:get_locked_size() if x1 and x2 then local dsx = (x1 < 1 or x2 < 1) and 0 or style.divider_size local dsy = (y1 < 1 or y2 < 1) and 0 or style.divider_size @@ -334,7 +303,6 @@ function Node:get_locked_size() 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 @@ -359,21 +327,21 @@ local function calc_split_sizes(self, x, y, x1, x2) self.divider = n / self.size[x] 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] + self.nodes[Node.LEFT].position[x] = self.position[x] + self.nodes[Node.LEFT].position[y] = self.position[y] + self.nodes[Node.LEFT].size[x] = n - ds + self.nodes[Node.LEFT].size[y] = self.size[y] + self.nodes[Node.RIGHT].position[x] = self.position[x] + n + self.nodes[Node.RIGHT].position[y] = self.position[y] + self.nodes[Node.RIGHT].size[x] = self.size[x] - n + self.nodes[Node.RIGHT].size[y] = self.size[y] end function Node:update_layout() if self.type == "leaf" then local av = self.active_view - if #self.views > 1 then + if #self.nodes > 1 then local _, th, _, _, viewport_w = self:get_tab_metrics() if self.tab_viewport_width ~= viewport_w then self.tab_viewport_width = viewport_w @@ -385,31 +353,27 @@ function Node:update_layout() copy_position_and_size(av, self) end else - local x1, y1 = self.a:get_locked_size() - local x2, y2 = self.b:get_locked_size() + local x1, y1 = self.nodes[Node.LEFT]:get_locked_size() + local x2, y2 = self.nodes[Node.RIGHT]: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() + self:propagate("update_layout") end end - function Node:update() if self.type == "leaf" then - for _, view in ipairs(self.views) do + for _, view in ipairs(self.nodes) do view:update() end else - self.a:update() - self.b:update() + self:propagate("update") end end - function Node:draw_tabs() local tw, h, overflow, viewport_x, viewport_w, max_scroll = self:get_tab_metrics() local x, y = self.position.x, self.position.y @@ -419,7 +383,7 @@ function Node:draw_tabs() renderer.draw_rect(x, y + h - ds, self.size.x, ds, style.divider) core.push_clip_rect(viewport_x, y, viewport_w, h) - for i, view in ipairs(self.views) do + for i, view in ipairs(self.nodes) do local x = viewport_x + (i - 1) * tw - self.tab_scroll local w = tw local text = view:get_name() @@ -452,10 +416,9 @@ function Node:draw_tabs() core.pop_clip_rect() end - function Node:draw() if self.type == "leaf" then - if #self.views > 1 then + if #self.nodes > 1 then self:draw_tabs() end local pos, size = self.active_view.position, self.active_view.size @@ -469,8 +432,6 @@ function Node:draw() end end - - local RootView = View:extend() function RootView:new() @@ -481,12 +442,10 @@ function RootView:new() self.mouse = { x = 0, y = 0 } end - function RootView:defer_draw(fn, ...) table.insert(self.deferred_draws, 1, { fn = fn, ... }) end - -- Floating views are drawn on top of the node tree, positioned freely -- (independent of the split layout). While a floating view reports itself -- as shown, it captures input as a modal overlay. @@ -495,7 +454,6 @@ function RootView:add_floating_view(view) table.insert(self.floating_views, view) end - function RootView:get_active_floating_view() for _, view in ipairs(self.floating_views) do if view:is_shown() then @@ -504,12 +462,10 @@ function RootView:get_active_floating_view() end end - function RootView:get_active_node() return self.root_node:get_node_for_view(core.active_view) end - function RootView:open_view(view, is_same) local node = self:get_active_node() if node.locked and core.last_active_view then @@ -518,7 +474,7 @@ function RootView:open_view(view, is_same) end assert(not node.locked, "Cannot open view on locked node") if is_same then - for _, existing in ipairs(node.views) do + for _, existing in ipairs(node.nodes) do if is_same(existing) then node:set_active_view(existing) return existing, false @@ -530,7 +486,6 @@ function RootView:open_view(view, is_same) return view, true end - function RootView:open_doc(doc, ViewType) ViewType = ViewType or DocView local view, is_new = self:open_view(ViewType(doc), function(existing) @@ -542,7 +497,6 @@ function RootView:open_doc(doc, ViewType) return view end - function RootView:on_mouse_pressed(button, x, y, clicks) -- a shown floating view is modal: it captures the click, and a click -- outside its bounds dismisses it @@ -568,9 +522,9 @@ function RootView:on_mouse_pressed(button, x, y, clicks) if idx then local close_idx = node:get_tab_close_overlapping_point(x, y) if button == "left" and close_idx then - node:close_view(self.root_node, node.views[close_idx]) + node:close_view(self.root_node, node.nodes[close_idx]) else - node:set_active_view(node.views[idx]) + node:set_active_view(node.nodes[idx]) end if button == "middle" then node:close_active_view(self.root_node) @@ -581,7 +535,6 @@ function RootView:on_mouse_pressed(button, x, y, clicks) end end - function RootView:on_mouse_released(...) if self.dragged_divider then self.dragged_divider = nil @@ -590,7 +543,6 @@ function RootView:on_mouse_released(...) self.root_node:on_mouse_released(...) end - function RootView:on_mouse_moved(x, y, dx, dy) self.mouse.x, self.mouse.y = x, y local fv = self:get_active_floating_view() @@ -614,14 +566,14 @@ function RootView:on_mouse_moved(x, y, dx, dy) end node.divider = common.clamp(node.divider, 0.01, 0.99) - local ax, ay = node.a:get_locked_size() - local bx, by = node.b:get_locked_size() + local ax, ay = node.nodes[Node.LEFT]:get_locked_size() + local bx, by = node.nodes[Node.RIGHT]:get_locked_size() if node.type == "hsplit" then - if ax then node.a.active_view.size.x = node.size.x * node.divider - divider_size end - if bx then node.b.active_view.size.x = node.size.x * (1 - node.divider) - divider_size end + if ax then node.nodes[Node.LEFT].active_view.size.x = node.size.x * node.divider - divider_size end + if bx then node.nodes[Node.RIGHT].active_view.size.x = node.size.x * (1 - node.divider) - divider_size end else - if ay then node.a.active_view.size.y = node.size.y * node.divider - divider_size end - if by then node.b.active_view.size.y = node.size.y * (1 - node.divider) - divider_size end + if ay then node.nodes[Node.LEFT].active_view.size.y = node.size.y * node.divider - divider_size end + if by then node.nodes[Node.RIGHT].active_view.size.y = node.size.y * (1 - node.divider) - divider_size end end end @@ -657,7 +609,6 @@ function RootView:on_mouse_moved(x, y, dx, dy) end end - function RootView:on_mouse_wheel(delta, ...) local fv = self:get_active_floating_view() if fv then @@ -676,12 +627,10 @@ function RootView:on_mouse_wheel(delta, ...) node.active_view:on_mouse_wheel(delta, ...) end - function RootView:on_text_input(...) core.active_view:on_text_input(...) end - function RootView:update() copy_position_and_size(self.root_node, self) self.root_node:update() @@ -691,7 +640,6 @@ function RootView:update() end end - function RootView:draw() self.root_node:draw() while #self.deferred_draws > 0 do @@ -710,5 +658,4 @@ function RootView:draw() end end - return RootView diff --git a/data/core/object.lua b/data/core/utils/object.lua similarity index 100% rename from data/core/object.lua rename to data/core/utils/object.lua diff --git a/data/core/utils/tree.lua b/data/core/utils/tree.lua new file mode 100644 index 0000000..2024eed --- /dev/null +++ b/data/core/utils/tree.lua @@ -0,0 +1,68 @@ +local Object = require "core.utils.object" + + +local Tree = Object:extend() + +function Tree:new() + self.nodes = {} +end + +function Tree:propagate(fn, ...) + for _, node in ipairs(self.nodes or {}) do + node[fn](node, ...) + end +end + +function Tree:consume(node) + for k, _ in pairs(self) do self[k] = nil end + for k, v in pairs(node) do self[k] = v end +end + +function Tree:add_child(node, pos) + local nodes = self.nodes or {} + assert(pos == (#nodes + 1), + string.format("Tried to add out of order (pos: %d | curr size: %d)", pos, #nodes)) + assert(nodes[pos] == nil, string.format("Tried to add to non empty node (%d)", pos)) + local child = Tree() + child:consume(node) + nodes[pos] = child + self.nodes = nodes + return child +end + +function Tree:del_child(pos) + assert(self.nodes and self.nodes[pos] ~= nil, string.format("Tried to delete empty node (%d)", pos)) + local children = self.nodes[pos].nodes or {} + table.remove(self.nodes, pos) + for i, child in ipairs(children) do + table.insert(self.nodes, pos + i - 1, child) + end +end + + +function Tree:get_parent(root) + for _, node in ipairs(root.nodes or {}) do + if node == self then + return root + end + end + for _, node in ipairs(root.nodes or {}) do + local found = self:get_parent(node) + if found then return found end + end +end + + +function Tree:get_children(t) + t = t or {} + if not self.nodes or #self.nodes == 0 then + table.insert(t, self) + else + for _, node in ipairs(self.nodes) do + node:get_children(t) + end + end + return t +end + +return Tree diff --git a/data/core/view.lua b/data/core/view.lua index ea21f02..fcb301a 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -2,7 +2,7 @@ local core = require "core" local config = require "core.config" local style = require "core.style" local common = require "core.common" -local Object = require "core.object" +local Object = require "core.utils.object" local View = Object:extend() From 3bcdda3acd4ac4910c0734c7b7861dd17a2bc849 Mon Sep 17 00:00:00 2001 From: tacf Date: Sun, 16 Aug 2026 23:15:54 +0100 Subject: [PATCH 02/12] tweak: add_child using insert for simplified N-ary tree handling --- data/core/commands/root.lua | 8 ++++---- data/core/rootview.lua | 15 +++++++-------- data/core/utils/tree.lua | 12 +++++------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 3e37282..092f6f5 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -31,8 +31,8 @@ local t = { local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) if idx > 1 then - table.remove(node.nodes, idx) - table.insert(node.nodes, idx - 1, core.active_view) + node:del_child(idx) + node:add_child(core.active_view, idx - 1) node:scroll_tab_into_view(core.active_view) end end, @@ -41,8 +41,8 @@ local t = { local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) if idx < #node.nodes then - table.remove(node.nodes, idx) - table.insert(node.nodes, idx + 1, core.active_view) + node:del_child(idx) + node:add_child(core.active_view, idx + 1) node:scroll_tab_into_view(core.active_view) end end, diff --git a/data/core/rootview.lua b/data/core/rootview.lua index e7a30bd..49e6efd 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -92,13 +92,12 @@ function Node:split(dir, view, locked) other.locked = locked core.set_active_view(last_active) end - self.nodes = {} if dir == "up" or dir == "left" then - self.nodes[Node.LEFT] = other - self.nodes[Node.RIGHT] = child + self:add_child(other) + self:add_child(child) else - self.nodes[Node.LEFT] = child - self.nodes[Node.RIGHT] = other + self:add_child(child) + self:add_child(other) end return child end @@ -108,7 +107,7 @@ function Node:close_view(root, view) local idx = self:get_view_idx(view) if not idx then return end if #self.nodes > 1 then - table.remove(self.nodes, idx) + self:del_child(idx) if view == self.active_view then self:set_active_view(self.nodes[idx] or self.nodes[#self.nodes]) else @@ -144,9 +143,9 @@ function Node:add_view(view) 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.nodes[1] and self.nodes[1]:is(EmptyView) then - table.remove(self.nodes) + self:del_child(1) end - table.insert(self.nodes, view) + self:add_child(view) self:set_active_view(view) end diff --git a/data/core/utils/tree.lua b/data/core/utils/tree.lua index 2024eed..dfcb08f 100644 --- a/data/core/utils/tree.lua +++ b/data/core/utils/tree.lua @@ -18,14 +18,12 @@ function Tree:consume(node) for k, v in pairs(node) do self[k] = v end end -function Tree:add_child(node, pos) +function Tree:add_child(child, pos) local nodes = self.nodes or {} - assert(pos == (#nodes + 1), - string.format("Tried to add out of order (pos: %d | curr size: %d)", pos, #nodes)) - assert(nodes[pos] == nil, string.format("Tried to add to non empty node (%d)", pos)) - local child = Tree() - child:consume(node) - nodes[pos] = child + pos = pos or (#nodes + 1) + assert(pos >= 1 and pos <= #nodes + 1, + string.format("Tried to add out of range (pos: %d | curr size: %d)", pos, #nodes)) + table.insert(nodes, pos, child) self.nodes = nodes return child end From f5beb8d3fe7fbd3741f93d5cbd8602aa1dbe269e Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 13:53:20 +0100 Subject: [PATCH 03/12] feat: inject some GPL-1 into the binary --- CMakeLists.txt | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bce31b9..7f5c580 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,18 +38,33 @@ if(NSLITE_USE_SYSTEM_LIBS) set(NSLITE_GIT_INCLUDE_DIRS "") else() include(FetchContent) - + FetchContent_Declare( SDL3 GIT_REPOSITORY https://github.com/libsdl-org/SDL.git GIT_TAG main GIT_SHALLOW TRUE ) - - set(SDL_SHARED ON CACHE BOOL "" FORCE) - set(SDL_STATIC OFF CACHE BOOL "" FORCE) + + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) + set(SDL_LEAN_AND_MEAN ON CACHE BOOL "" FORCE) set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE) - + + set(SDL_AUDIO OFF CACHE BOOL "" FORCE) + set(SDL_RENDER OFF CACHE BOOL "" FORCE) + set(SDL_GPU OFF CACHE BOOL "" FORCE) + set(SDL_CAMERA OFF CACHE BOOL "" FORCE) + set(SDL_JOYSTICK OFF CACHE BOOL "" FORCE) + set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) + set(SDL_HIDAPI OFF CACHE BOOL "" FORCE) + set(SDL_SENSOR OFF CACHE BOOL "" FORCE) + set(SDL_OPENGL OFF CACHE BOOL "" FORCE) + set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) + set(SDL_VULKAN OFF CACHE BOOL "" FORCE) + set(SDL_DBUS OFF CACHE BOOL "" FORCE) + set(SDL_LIBUDEV OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(SDL3) set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) @@ -60,32 +75,32 @@ else() set(PCRE2_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(PCRE2_SUPPORT_JIT OFF CACHE BOOL "" FORCE) set(PCRE2_SHOW_REPORT OFF CACHE BOOL "" FORCE) - + FetchContent_Declare( PCRE2 GIT_REPOSITORY https://github.com/PCRE2Project/pcre2.git GIT_TAG pcre2-10.47 GIT_SHALLOW TRUE ) - + FetchContent_MakeAvailable(PCRE2) - + FetchContent_Declare( lua URL https://www.lua.org/ftp/lua-5.5.0.tar.gz URL_HASH SHA256=57ccc32bbbd005cab75bcc52444052535af691789dba2b9016d5c50640d68b3d ) - + FetchContent_MakeAvailable(lua) - + file(GLOB LUA_SOURCES ${lua_SOURCE_DIR}/src/*.c) list(FILTER LUA_SOURCES EXCLUDE REGEX "lua\\.c$") list(FILTER LUA_SOURCES EXCLUDE REGEX "luac\\.c$") - + add_library(lua STATIC ${LUA_SOURCES}) target_include_directories(lua PUBLIC ${lua_SOURCE_DIR}/src) set_target_properties(lua PROPERTIES POSITION_INDEPENDENT_CODE ON) - + if(UNIX AND NOT APPLE) target_compile_definitions(lua PRIVATE LUA_USE_POSIX) elseif(WIN32) From 36e21414eeda6c09d17f3c2c2e0b077310a87240 Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 14:12:40 +0100 Subject: [PATCH 04/12] refactor: move EmptyView to it's own file + bind debug mode toggle --- data/core/commands/core.lua | 7 ++++++ data/core/keymap.lua | 1 + data/core/presentations/empty.lua | 37 +++++++++++++++++++++++++++++++ data/core/rootview.lua | 34 +--------------------------- 4 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 data/core/presentations/empty.lua diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 080f427..094425a 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -3,9 +3,11 @@ local common = require "core.common" local command = require "core.command" local keymap = require "core.keymap" local LogView = require "core.logview" +local renderer = require "renderer" local fullscreen = false +local debug_enabled = false command.add(nil, { ["core:quit"] = function() @@ -88,6 +90,11 @@ command.add(nil, { core.open_file(USERDIR .. PATHSEP .. "init.lua") end, + ["core:toggle-debug"] = function() + debug_enabled = not debug_enabled + renderer.show_debug(debug_enabled) + end, + ["core:open-project-module"] = function() local filename = core.project_path(".nslite_project.lua") if system.get_file_info(filename) then diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 1226145..e284d5c 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -104,6 +104,7 @@ keymap.add { ["mod+o"] = "core:open-file", ["mod+n"] = "core:new-doc", ["alt+return"] = "core:toggle-fullscreen", + ["f12"] = "core:toggle-debug", ["alt+shift+j"] = "root:split-left", ["alt+shift+l"] = "root:split-right", diff --git a/data/core/presentations/empty.lua b/data/core/presentations/empty.lua new file mode 100644 index 0000000..6da5c8d --- /dev/null +++ b/data/core/presentations/empty.lua @@ -0,0 +1,37 @@ +local keymap = require "core.keymap" +local style = require "core.style" +local View = require "core.view" + + +local EmptyView = View:extend() + +local function draw_text(x, y, color) + local th = style.big_font:get_height() + local dh = th + style.padding.y * 2 + x = renderer.draw_text(style.big_font, "nslite", x, y + (dh - th) / 2, 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" }, + } + th = style.font:get_height() + y = y + (dh - th * 2 - style.padding.y) / 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 + +return EmptyView diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 49e6efd..5f0b146 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -1,42 +1,10 @@ local core = require "core" local common = require "core.common" local style = require "core.style" -local keymap = require "core.keymap" local Tree = require "core.utils.tree" local View = require "core.view" 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 = th + style.padding.y * 2 - x = renderer.draw_text(style.big_font, "nslite", x, y + (dh - th) / 2, 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" }, - } - th = style.font:get_height() - y = y + (dh - th * 2 - style.padding.y) / 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 EmptyView = require "core.presentations.empty" local Node = Tree:extend() From 90a52c0e1af2b1441e699d9209fa7ab470066639 Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 14:20:28 +0100 Subject: [PATCH 05/12] chore: who left this nasty little dup here? --- data/core/common.lua | 5 +++++ data/core/keymap.lua | 4 ++-- data/plugins/lsp.lua | 10 +++------- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/data/core/common.lua b/data/core/common.lua index e7e81eb..ac1b736 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -1,6 +1,11 @@ local common = {} +function common.primary_modifier() + return PLATFORM == "macOS" and "cmd" or "ctrl" +end + + function common.is_utf8_cont(char) local byte = char:byte() return byte >= 0x80 and byte < 0xc0 diff --git a/data/core/keymap.lua b/data/core/keymap.lua index e284d5c..2d74ca3 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -1,4 +1,5 @@ local command = require "core.command" +local common = require "core.common" local keymap = {} keymap.modkeys = {} @@ -35,8 +36,7 @@ end local function resolve_mod(stroke) -- Default bindings use "mod" for the platform's primary shortcut modifier. - local modifier = PLATFORM == "macOS" and "cmd" or "ctrl" - return stroke:gsub("mod%+", modifier .. "+") + return stroke:gsub("mod%+", common.primary_modifier() .. "+") end diff --git a/data/plugins/lsp.lua b/data/plugins/lsp.lua index 8aa02e0..88fe00c 100644 --- a/data/plugins/lsp.lua +++ b/data/plugins/lsp.lua @@ -1,4 +1,5 @@ local core = require "core" +local common = require "core.common" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" @@ -8,11 +9,6 @@ local StatusView = require "core.statusview" local clients = setmetatable({}, { __mode = "k" }) -local function primary_modifier_down() - return system.key_modifier_down(PLATFORM == "macOS" and "cmd" or "ctrl") -end - - local function matching_server(filename) local settings = config.lsp if not settings then return end @@ -103,7 +99,7 @@ local function update_link(view) local mouse = core.root_view.mouse local pointer_over_text = pointer_is_over_text(view, mouse.x, mouse.y) local filename = view.doc:get_filename() - if not primary_modifier_down() or not pointer_over_text or not filename then + if not system.key_modifier_down(common.primary_modifier()) or not pointer_over_text or not filename then clear_link(view, pointer_over_text) return end @@ -166,7 +162,7 @@ end local docview_mouse_pressed = DocView.on_mouse_pressed function DocView:on_mouse_pressed(button, x, y, clicks) - if button == "left" and primary_modifier_down() then + if button == "left" and system.key_modifier_down(common.primary_modifier()) then update_link(self) local link = self.lsp_link if link and link.revision == self.doc:get_revision() then From f6d2e632c412c6b588120ff6182b5982ebb825a2 Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 14:39:14 +0100 Subject: [PATCH 06/12] chore: move logview into presentations --- data/core/commands/core.lua | 2 +- data/core/{logview.lua => presentations/log.lua} | 0 data/core/statusview.lua | 13 +++---------- 3 files changed, 4 insertions(+), 11 deletions(-) rename data/core/{logview.lua => presentations/log.lua} (100%) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 094425a..183725a 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -2,7 +2,7 @@ local core = require "core" local common = require "core.common" local command = require "core.command" local keymap = require "core.keymap" -local LogView = require "core.logview" +local LogView = require "core.presentations.log" local renderer = require "renderer" diff --git a/data/core/logview.lua b/data/core/presentations/log.lua similarity index 100% rename from data/core/logview.lua rename to data/core/presentations/log.lua diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 5f1aed1..14ab27d 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -4,12 +4,12 @@ local command = require "core.command" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" -local LogView = require "core.logview" +local LogView = require "core.presentations.log" local ImageView = require "core.presentations.image" local View = require "core.view" -local StatusView = View:extend() +local StatusView = View:extend() StatusView.separator = " " StatusView.separator2 = " | " @@ -21,16 +21,14 @@ function StatusView:new() self.message = {} end - function StatusView:on_mouse_pressed() core.set_active_view(core.last_active_view) if system.get_time() < self.message_timeout - and not core.active_view:is(LogView) then + and not core.active_view:is(LogView) then command.perform "core:open-log" end end - function StatusView:show_message(icon, icon_color, text) self.message = { icon_color, style.icon_font, icon, @@ -39,7 +37,6 @@ function StatusView:show_message(icon, icon_color, text) self.message_timeout = system.get_time() + config.message_timeout end - function StatusView:update() self.size.y = style.font:get_height() + style.padding.y * 2 @@ -52,7 +49,6 @@ function StatusView:update() StatusView.super.update(self) end - local function draw_items(self, items, x, y, draw_fn) local font = style.font local color = style.text @@ -89,7 +85,6 @@ function StatusView:draw_items(items, right_align, yoffset) end end - local function get_active_docview() local view = core.active_view -- We present status view only for subclasses of DocView @@ -160,7 +155,6 @@ function StatusView:get_items() } end - function StatusView:draw() self:draw_background(style.background2) @@ -173,5 +167,4 @@ function StatusView:draw() self:draw_items(right, true) end - return StatusView From 5f2cdcaae75bfb7009348bf1d93009093919cf95 Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 22:55:55 +0100 Subject: [PATCH 07/12] feat: Custom rendered title bar (configurable - on by default) + some crazy window jutsu for transparency --- CMakeLists.txt | 4 +- data/core/commands/core.lua | 18 ++++ data/core/config.lua | 2 + data/core/init.lua | 10 ++ data/core/presentations/titlebar.lua | 149 +++++++++++++++++++++++++++ data/core/rootview.lua | 43 +++++++- data/fonts/icons.ttf | Bin 7644 -> 7308 bytes src/api/renderer.c | 26 ++--- src/api/renderer_image.c | 24 ++++- src/api/system.c | 103 +++++++++++++++--- src/main.c | 35 ++++++- src/rencache.c | 121 +++++++++++++--------- src/rencache.h | 5 +- src/renderer.c | 53 +++++++--- src/renderer.h | 2 + 15 files changed, 495 insertions(+), 100 deletions(-) create mode 100644 data/core/presentations/titlebar.lua diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f5c580..eff75ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,9 @@ else() set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) set(SDL_HIDAPI OFF CACHE BOOL "" FORCE) set(SDL_SENSOR OFF CACHE BOOL "" FORCE) - set(SDL_OPENGL OFF CACHE BOOL "" FORCE) + # Only to compile in X11's visual selection; without GLX it forces the + # default depth-24 visual (XRGB8888, no alpha). Rendering stays software. + set(SDL_OPENGL ON CACHE BOOL "" FORCE) set(SDL_OPENGLES OFF CACHE BOOL "" FORCE) set(SDL_VULKAN OFF CACHE BOOL "" FORCE) set(SDL_DBUS OFF CACHE BOOL "" FORCE) diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 183725a..74fdd82 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -1,8 +1,11 @@ local core = require "core" local common = require "core.common" local command = require "core.command" +local config = require "core.config" local keymap = require "core.keymap" +local style = require "core.style" local LogView = require "core.presentations.log" +local TitleBarView = require "core.presentations.titlebar" local renderer = require "renderer" @@ -105,4 +108,19 @@ command.add(nil, { doc:save(filename) end end, + + ["core:toggle-native-titlebar"] = function() + config.native_title_bar = not config.native_title_bar + if config.native_title_bar then + system.configure_titlebar(0, 0) + core.root_view.titlebar = nil + else + local th = style.font:get_height() + style.padding.y * 2 + local bm = 30 * 3 + style.padding.x + system.configure_titlebar(th, bm) + core.root_view.titlebar = TitleBarView() + core.root_view.titlebar:set_title(core.window_title or "nslite") + end + core.redraw = true + end, }) diff --git a/data/core/config.lua b/data/core/config.lua index e1d685f..9e15507 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -17,6 +17,8 @@ config.indent_size = 2 config.tab_type = "soft" config.line_limit = 80 +config.native_title_bar = false + -- Language servers start lazily, the first time mod is held over a matching -- document. Add or replace entries from user/init.lua as needed. config.lsp = { diff --git a/data/core/init.lua b/data/core/init.lua index 358bbc9..fc232c2 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -112,6 +112,13 @@ function core.init() core.find_view = FindView() core.status_view = StatusView() + -- apply borderless window when using own title bar + if not config.native_title_bar then + local th = style.font:get_height() + style.padding.y * 2 + local bm = 30 * 3 + style.padding.x + system.configure_titlebar(th, bm) + end + core.root_view.root_node:split("down", core.status_view, true) core.root_view:add_floating_view(core.command_view) core.root_view:add_floating_view(core.find_view) @@ -428,6 +435,9 @@ function core.step() if title ~= core.window_title then system.set_window_title(title) core.window_title = title + if core.root_view.titlebar then + core.root_view.titlebar:set_title(title) + end end -- draw diff --git a/data/core/presentations/titlebar.lua b/data/core/presentations/titlebar.lua new file mode 100644 index 0000000..7650bf4 --- /dev/null +++ b/data/core/presentations/titlebar.lua @@ -0,0 +1,149 @@ +local core = require "core" +local common = require "core.common" +local style = require "core.style" +local View = require "core.view" + + +local TitleBarView = View:extend() + +local BUTTON_SIZE = 30 +local ICON_PADDING = 8 +local ICON_SIZE = 30 + +local app_icon = nil + + +function TitleBarView:new() + TitleBarView.super.new(self) + self.title = "" + self.maximized = false + self.hovered_button = nil + self.pressed_button = nil +end + +function TitleBarView:set_title(title) + self.title = title +end + +function TitleBarView:get_button_rect(index) + local x = self.position.x + self.size.x - (BUTTON_SIZE * (4 - index)) + local y = self.position.y + return x, y, BUTTON_SIZE, self.size.y +end + +function TitleBarView:get_button_at(x, y) + for i = 1, 3 do + local bx, by, bw, bh = self:get_button_rect(i) + if x >= bx and x < bx + bw and y >= by and y < by + bh then + return i + end + end + return nil +end + +function TitleBarView:on_mouse_pressed(button, x, y, clicks) + if button ~= "left" then return end + local btn = self:get_button_at(x, y) + if btn then + self.pressed_button = btn + return true + end + return false +end + +function TitleBarView:on_mouse_released(button, x, y) + if button ~= "left" then return end + if self.pressed_button then + local btn = self:get_button_at(x, y) + if btn == self.pressed_button then + if btn == 1 then + system.minimize_window() + elseif btn == 2 then + self:toggle_maximize() + elseif btn == 3 then + core.quit() + end + end + self.pressed_button = nil + return true + end + return false +end + +function TitleBarView:on_mouse_moved(x, y, dx, dy) + local btn = self:get_button_at(x, y) + if btn ~= self.hovered_button then + self.hovered_button = btn + core.redraw = true + end + return false +end + +function TitleBarView:toggle_maximize() + if self.maximized then + system.set_window_mode("normal") + else + system.set_window_mode("maximized") + end + self.maximized = not self.maximized +end + +function TitleBarView:update() + self.size.y = style.font:get_height() + style.padding.y * 2 + self.cursor = "arrow" + if self.hovered_button then + self.cursor = "hand" + end + TitleBarView.super.update(self) +end + +function TitleBarView:draw() + local h = self.size.y + local x = self.position.x + local y = self.position.y + local w = self.size.x + + self:draw_background(style.background2) + + -- divider at bottom + local ds = style.divider_size + renderer.draw_rect(x, y + h - ds, w, ds, style.divider) + + -- icon + if not app_icon and WINDOW_ICON then + app_icon = WINDOW_ICON + end + if app_icon then + local icon_x = x + style.padding.x + local icon_y = y + math.floor((h - ICON_SIZE) / 2) + renderer.draw_image(app_icon, icon_x, icon_y, ICON_SIZE, ICON_SIZE) + end + + -- title text + local title_x = x + style.padding.x * 2 + ICON_SIZE + ICON_PADDING + local buttons_area = BUTTON_SIZE * 3 + local title_w = math.max(0, w - (title_x - x) - buttons_area - style.padding.x) + local label, cropped = common.crop_text(style.font, self.title, title_w, "...") + common.draw_text(style.font, style.text, label, + cropped and "left" or "center", title_x, y, title_w, h) + + -- window control buttons + local button_labels = { "-", "^", "x" } + + for i = 1, 3 do + local bx, by, bw, bh = self:get_button_rect(i) + local is_hovered = self.hovered_button == i + local is_pressed = self.pressed_button == i + + local color = style.dim + if is_hovered then + color = style.accent + end + if is_pressed then + color = style.text + end + common.draw_text(style.icon_font, color, button_labels[i], "center", bx, by, bw, bh) + end +end + +return TitleBarView diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 5f0b146..4740aef 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -1,10 +1,12 @@ local core = require "core" local common = require "core.common" local style = require "core.style" +local config = require "core.config" local Tree = require "core.utils.tree" local View = require "core.view" local DocView = require "core.docview" local EmptyView = require "core.presentations.empty" +local TitleBarView = require "core.presentations.titlebar" local Node = Tree:extend() @@ -407,6 +409,9 @@ function RootView:new() self.deferred_draws = {} self.floating_views = {} self.mouse = { x = 0, y = 0 } + if not config.native_title_bar then + self.titlebar = TitleBarView() + end end function RootView:defer_draw(fn, ...) @@ -478,6 +483,13 @@ function RootView:on_mouse_pressed(button, x, y, clicks) return end + -- capture titlebar mouse input (for handling events when using engine + -- self drawn titlebar) + if self.titlebar and y < self.titlebar.position.y + self.titlebar.size.y then + self.titlebar:on_mouse_pressed(button, x, y, clicks) + return + end + local div = self.root_node:get_divider_overlapping_point(x, y) if div then self.dragged_divider = div @@ -502,12 +514,15 @@ function RootView:on_mouse_pressed(button, x, y, clicks) end end -function RootView:on_mouse_released(...) +function RootView:on_mouse_released(button, x, y) if self.dragged_divider then self.dragged_divider = nil self.resized_detached = nil end - self.root_node:on_mouse_released(...) + if self.titlebar and self.titlebar.pressed_button then + self.titlebar:on_mouse_released(button, x, y) + end + self.root_node:on_mouse_released(button, x, y) end function RootView:on_mouse_moved(x, y, dx, dy) @@ -519,6 +534,13 @@ function RootView:on_mouse_moved(x, y, dx, dy) return end + -- titlebar hover detection for cursor changes + if self.titlebar and y < self.titlebar.position.y + self.titlebar.size.y then + self.titlebar:on_mouse_moved(x, y, dx, dy) + system.set_cursor(self.titlebar.cursor) + return + end + if self.dragged_divider then local node = self.dragged_divider local divider_size = style.divider_size @@ -599,7 +621,19 @@ function RootView:on_text_input(...) end function RootView:update() - copy_position_and_size(self.root_node, self) + if self.titlebar then + self.titlebar.position.x = 0 + self.titlebar.position.y = 0 + self.titlebar.size.x = self.size.x + self.titlebar:update() + local th = self.titlebar.size.y + self.root_node.position.x = self.position.x + self.root_node.position.y = self.position.y + th + self.root_node.size.x = self.size.x + self.root_node.size.y = self.size.y - th + else + copy_position_and_size(self.root_node, self) + end self.root_node:update() self.root_node:update_layout() for _, view in ipairs(self.floating_views) do @@ -608,6 +642,9 @@ function RootView:update() end function RootView:draw() + if self.titlebar then + self.titlebar:draw() + end self.root_node:draw() while #self.deferred_draws > 0 do local t = table.remove(self.deferred_draws) diff --git a/data/fonts/icons.ttf b/data/fonts/icons.ttf index d74fe7375b8a975558fcd29eefab25f8b1ceac70..185e687c5640aa744f5e0ff4eb52d0de1bc6ef04 100644 GIT binary patch delta 2532 zcmZuzeT-CB6+h?R_x?^FbubUHhw9=?fK4VaM9pm+GD&Q5}HjP^THPh_dP*QI_W@!=rwNWh>MhQZlQ z%x>5iiIDxNbr83%>fkm|**+M7%Vc?`Ym{f`1|;LJ&`|=?zteNL(-C|t%|@C&0n5pu zER4bg+z$`KSIP3;PKRt~$<_p{C)s1r5VFn!K<5HIuhTAcUW%L}oFJel1vw!VDFHGl zbWJgIFp0*%B5H#pL?V=^M7M|5IaXRGflTaYh8l5XLT}y6!#*QoO^8`!BpPH%91I@q zKiA4|<-}ROD%PBV18^FC3%76$&fy`v$b9xqc9ox$0_kB==X8m;`E`neL)WVT431Ze zBbZZ`VGt%ODK_jLA5y9B+5fwvHw2rJH=geLF*z}^{USjm=?hy1Xa45c3bRBCudap_1r=6R=6 zTL#%9amzc?UOB!if0`lUROLiv3dgF0dA!FDuB3xN(c+$eB|W5Q$G==%eub+tW2ws2 zRK+hAL=I5uzRWXJ8al*b8J%BFh6yk4RV(~6t7T2hk5?<~Za2xo$v!sbfp^CXaE5r`;; z6a=QEThGLwY6_2c@`osElh6-CbV4XR4n#_piY2dFuhj{$FsUXgMaq%5mlyK!tj8;^ zY4#_V{G@B6=Y)7G>`gmU@5}0O<=izPY;R`Ix;=fVrDv1-}jW`>f&2$ zP1AD{*B6i7hVCtXgWF(G`D~yhpx}$R51=|oQ$_eCqDkiumg?k@tVc{yWC;P$)s089 zP*u55&E=TZuj-FhdCl^W#U0CEjCqdTUZ|A|F{z1K`&Rp>v6LMnh*W%1OUO)0{cFMS zC2l(jbI}eg2h&bX8JF#ink2o4$Cf?{H6t#|mcgc78MT*j(&cE_ zam#JL5Jvq)^oACHkq;BfC2EC~7Zm|&(2VLo7hzfz_f6)gKLm*&8HxrmUgbpVCAx{I z1mg3wuOaPr9(y*RQ z*VnFgt8rZ^Xnc6zx4P=h|GxM_U!sp;uk9uixz$T=hW*u4!PeYYa^XSIwjgsU}*ON|!fKP~J3f7zVCwTu*Q>m?I4QJ;}H z8z}`9!XYalh|}H2LsNOZu^q9T;3%8rkTm3L@i(l5bfi zmlzGzh?%BXFlJayBW5#XRP35!=rZSuDVw@(x(OA3zC7v3F9k!@cApcI?fV#aZC8Gc zCzeh!MZ?XSrmz@O^!6Jntv<&!6*tq0o3tXC&Do?Mx=vdyq*AzMB=9}YvAC|wRGPhs zOxm(7eMXUWE6DWQre(^CVcD6StDB?}Q%%p%O~coycOq49CXuw9wz95Bl~mGon-vX5 z;+m%#sLD#AJDrH8*2UBX%(o8r)st`ip(mT!zrvk%oL66UoN0H3d-N!gX;;i7 z9(9f$J(2Id%6h1SQjZ}-=ws+27YEb#^j=h3nPuO&4*}`x?U07}?gF(~YPiI4@Q8}9 VGowcgp#gtA@ael^zVpw@e*pUb!9)N6 delta 2942 zcmZ`*e{2)?6@Tyhz5Cwza`v6=^PS_E;MmSNkPuSaaVRkGfDi(innH%c)*ngvIZ8+h zh|#ISQZ;Q|#UJe=RjfsbPC(UZ6ehNHVhEuM(M_uM2kkViz%(HhQH?Ry76hH++dBse zQ#D)nzTfxW_xtXBKkvu)_C0!OK?M*1Od=A%^5sL{`pJhcuL78HmZZtPWxdN`7C6Ko zBEI>tHFNW8%On2+z?X<$yMAnR!v3W2Dgeup?BM#{yHdfc{5b&QH0irGO>7=}`PEm} z0q}DG4HKJ3cTSMpBzya5R5x#XzOw1Wn)eNokx;X?Y#iO-q+TkJF`X8awva%1g8zc} zWyEK&Hg-OkfNjCH)rSQ`<+!HV#d^MYa>< zAUiiPzH`?nGglm;vt1+ov56g(jT4{tE;&mEiX`|^9Ss8G_RT-teA-xe3ubbEA^i38 zbpQR)yWwq>8u@DCRbal9ezb?L_O=p?b4l+>jde@Yx@j7&OTw@NcwN9C0kgr_Nz6`o zqb@>rZE_#3pK}W?k;NhJ1{w2sisl#AnSWPkL>%pIbZ2EJt_4|@l_1K$37 zN3bWq$zxmtY4R+4WQGcwRCo3Pk?T_dpwP->EC3BU1gF&@0@k0Ir1|m5^Sq!}w!=^1 z47TBm_*?vpwXwbIHs2?0kj}^&aPOA)EB-lQYmmDIm{;y@3Ny|E$Pic`FqzAYkNF59 zhY^rva|Iwu!yrk9)bGs8<=l*r6YZ_tMYkQ(Vg_2aQ^=Qc(PGXQD54T`EW3w99Bn0p zN=w?!xjncD^EO+cDccmSPEeB-9CW>1~UX3ad?aoy}RubXDJN#GJ6 z2V_7Vjghb0p~6@l+92z1Dz|)HFOhzTpl8FtBsQ?_-OT(EUCvtsIAtpXzhp-!OZ+59bA_PIFid;G(BIV5 zlxfODU8}pK_*v78v9KkSieHqJB|VfNjuI=lboyvGL&ENsFfW*PvojTOnlWYL7k0^5 zc|lf=i}Tk=Qd`6Jn#K6k-ny2*K!a_%7sBGWQa!;IredE~k5ZSp|A@km$=tFYq_SU!$T$CzrCMvBZ+aYX4 zCY@`|^*FX82kJ~G_F&0h5nY6it#FY`&(^e#XNtTOa`jZYsz*Yh+Lcm1t4ml>YhToU zt|xTeq~@|)wIY$*SG26@CaQ54lUnC&Ej}peER;%J(X~sVK~<7Iz@t+i+p6Y<*nlYs z{QxdEg&ucnuVv$O;;Q!wf1P(wX+){y-zW!lWGFCP~kj5ZJ5EZN)1c*!6@8ce{VS{Xaxs0^3X==&O@}n9(@T1)|F?*EWfPW zY^u4b(V7*TWrhM#Ejr!#8vN2KRj4mXlsQgX#Dq<~y1U5prJfw2O6i|T3&klF@`N&; zKZB;I-LCzxc0-{4+3Zf1PPv`GZr)ambj3s3>_k`d?s;a>*7adCd=UbUik$v5VAt8%QJZXSOB z>Y)Sm6y5HU-zIxXZhfrKq-R=EbQj@bMh`AxPORiN(tCS_V-r{EiI#G+Z?HtYe6l+C8i3k5|D+UP*`S6 z!MA6127hL?6>Cj~E{B#fZZ^wrNf?_t#e|A$)DY8|2-J=#AvT|@3a(K?yn!aZgtNYx zvuaAV3m0~@rj4K?hv=3}#1>Lm8v;RVktN5;fnaD!OtR>vZ3t_Y7EyB=`6rj#YGR3t zVIvm_F*g`h!wTKBB3q{?eLDG~^GD214#d;gIyoNC^xEtrW5Db%4<9BnVEQ8s_j$uO zeAt+<%>lzWa>V%hLZj1P04G0;Uu901Iuui_@% diff --git a/src/api/renderer.c b/src/api/renderer.c index b43837e..459c133 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -25,9 +25,8 @@ static uint8_t check_color_component(lua_State *L, int index) { static RenColor checkcolor(lua_State *L, int idx, uint8_t default_value) { RenColor color; if (lua_isnoneornil(L, idx)) { - return (RenColor) { - default_value, default_value, default_value, UINT8_MAX - }; + return ( + RenColor) { default_value, default_value, default_value, UINT8_MAX }; } lua_rawgeti(L, idx, 1); lua_rawgeti(L, idx, 2); @@ -52,8 +51,9 @@ static int f_show_debug(lua_State *L) { static int f_get_size(lua_State *L) { int w, h; ren_get_size(&w, &h); - lua_pushnumber(L, w); - lua_pushnumber(L, h); + int shadow = rencache_get_shadow(); + lua_pushnumber(L, w - shadow * 2); + lua_pushnumber(L, h - shadow * 2); return 2; } @@ -119,17 +119,11 @@ static int f_draw_image(lua_State *L) { } -static const luaL_Reg lib[] = { - { "show_debug", f_show_debug }, - { "get_size", f_get_size }, - { "begin_frame", f_begin_frame }, - { "end_frame", f_end_frame }, - { "set_clip_rect", f_set_clip_rect }, - { "draw_rect", f_draw_rect }, - { "draw_text", f_draw_text }, - { "draw_image", f_draw_image }, - { NULL, NULL } -}; +static const luaL_Reg lib[] = { { "show_debug", f_show_debug }, + { "get_size", f_get_size }, { "begin_frame", f_begin_frame }, + { "end_frame", f_end_frame }, { "set_clip_rect", f_set_clip_rect }, + { "draw_rect", f_draw_rect }, { "draw_text", f_draw_text }, + { "draw_image", f_draw_image }, { NULL, NULL } }; int luaopen_renderer_font(lua_State *L); diff --git a/src/api/renderer_image.c b/src/api/renderer_image.c index b2be169..1bd97e5 100644 --- a/src/api/renderer_image.c +++ b/src/api/renderer_image.c @@ -40,11 +40,27 @@ static int f_get_height(lua_State *L) { } +static int f_new_from_rgba(lua_State *L) { + int width = luaL_checkinteger(L, 1); + int height = luaL_checkinteger(L, 2); + size_t data_len; + const char *data = luaL_checklstring(L, 3, &data_len); + RenImage **self = lua_newuserdata(L, sizeof(*self)); + luaL_setmetatable(L, API_TYPE_IMAGE); + *self = ren_new_image_from_rgba(width, height, data, data_len); + if (!*self) { + return luaL_error(L, "invalid image data for %dx%d", width, height); + } + return 1; +} + + static const luaL_Reg lib[] = { - { "__gc", f_gc }, - { "load", f_load }, - { "get_width", f_get_width }, - { "get_height", f_get_height }, + { "__gc", f_gc }, + { "load", f_load }, + { "new_from_rgba", f_new_from_rgba }, + { "get_width", f_get_width }, + { "get_height", f_get_height }, { NULL, NULL } }; diff --git a/src/api/system.c b/src/api/system.c index 8c80495..a8ae2bc 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -33,12 +33,12 @@ static char *key_name( return destination; } - static int f_poll_event(lua_State *L) { char buf[16]; SDL_Event e; // events arrive in window points; the framebuffer is pixels double scale = window_get_pixel_density(window); + int shadow = rencache_get_shadow(); while (SDL_PollEvent(&e)) { switch (e.type) { @@ -48,8 +48,8 @@ static int f_poll_event(lua_State *L) { case SDL_EVENT_WINDOW_RESIZED: lua_pushstring(L, "resized"); - lua_pushnumber(L, e.window.data1 * scale); - lua_pushnumber(L, e.window.data2 * scale); + lua_pushnumber(L, (e.window.data1 - shadow * 2) * scale); + lua_pushnumber(L, (e.window.data2 - shadow * 2) * scale); return 3; case SDL_EVENT_WINDOW_EXPOSED: @@ -64,8 +64,8 @@ static int f_poll_event(lua_State *L) { case SDL_EVENT_DROP_FILE: lua_pushstring(L, "filedropped"); lua_pushstring(L, e.drop.data); - lua_pushnumber(L, e.drop.x * scale); - lua_pushnumber(L, e.drop.y * scale); + lua_pushnumber(L, (e.drop.x - shadow) * scale); + lua_pushnumber(L, (e.drop.y - shadow) * scale); return 4; case SDL_EVENT_KEY_DOWN: @@ -87,8 +87,8 @@ static int f_poll_event(lua_State *L) { if (e.button.button == 1) { SDL_CaptureMouse(true); } lua_pushstring(L, "mousepressed"); lua_pushstring(L, button_name(e.button.button)); - lua_pushnumber(L, e.button.x * scale); - lua_pushnumber(L, e.button.y * scale); + lua_pushnumber(L, (e.button.x - shadow) * scale); + lua_pushnumber(L, (e.button.y - shadow) * scale); lua_pushnumber(L, e.button.clicks); return 5; @@ -96,14 +96,14 @@ static int f_poll_event(lua_State *L) { if (e.button.button == 1) { SDL_CaptureMouse(false); } lua_pushstring(L, "mousereleased"); lua_pushstring(L, button_name(e.button.button)); - lua_pushnumber(L, e.button.x * scale); - lua_pushnumber(L, e.button.y * scale); + lua_pushnumber(L, (e.button.x - shadow) * scale); + lua_pushnumber(L, (e.button.y - shadow) * scale); return 4; case SDL_EVENT_MOUSE_MOTION: lua_pushstring(L, "mousemoved"); - lua_pushnumber(L, e.motion.x * scale); - lua_pushnumber(L, e.motion.y * scale); + lua_pushnumber(L, (e.motion.x - shadow) * scale); + lua_pushnumber(L, (e.motion.y - shadow) * scale); lua_pushnumber(L, e.motion.xrel * scale); lua_pushnumber(L, e.motion.yrel * scale); return 5; @@ -182,9 +182,82 @@ enum { WIN_NORMAL, WIN_MAXIMIZED, WIN_FULLSCREEN }; static int f_set_window_mode(lua_State *L) { int n = luaL_checkoption(L, 1, "normal", window_opts); - SDL_SetWindowFullscreen(window, n == WIN_FULLSCREEN); - if (n == WIN_NORMAL) { SDL_RestoreWindow(window); } - if (n == WIN_MAXIMIZED) { SDL_MaximizeWindow(window); } + if (n == WIN_FULLSCREEN) { + SDL_SetWindowFullscreen(window, true); + } else if (n == WIN_NORMAL) { + SDL_RestoreWindow(window); + } else if (n == WIN_MAXIMIZED) { + SDL_MaximizeWindow(window); + } + return 0; +} + + +static int f_minimize_window(lua_State *L) { + (void) L; + SDL_MinimizeWindow(window); + return 0; +} + + +static int g_titlebar_height = 0; +static int g_titlebar_drag_margin = 0; + +#define SHADOW_PX 8 + +static SDL_HitTestResult SDLCALL hit_test_cb( + SDL_Window *win, const SDL_Point *point, void *data) { + (void) data; + if (g_titlebar_height > 0) { + int inset = rencache_get_shadow(); + int top = inset; + int bottom = inset + g_titlebar_height; + int left = inset; + int w; + SDL_GetWindowSize(win, &w, NULL); + int right = w - inset - g_titlebar_drag_margin; + if (point->y >= top && point->y < bottom && point->x >= left + && point->x < right) { + return SDL_HITTEST_DRAGGABLE; + } + } + return SDL_HITTEST_NORMAL; +} + +/* configure_titlebar(height, button_margin): + * height > 0 → enable custom titlebar (borderless, shadow, hit test) + * height == 0 → disable custom titlebar (bordered, no shadow, no hit test) + */ +static int f_configure_titlebar(lua_State *L) { + int height = (int) luaL_checknumber(L, 1); + int margin = (int) luaL_checknumber(L, 2); + + int old_shadow = rencache_get_shadow(); + int new_shadow = height > 0 ? SHADOW_PX : 0; + + g_titlebar_height = height; + g_titlebar_drag_margin = margin; + + rencache_set_shadow(new_shadow); + + /* borderless mode */ + SDL_SetWindowBordered(window, height == 0); + + /* hit test */ + if (height > 0) { + SDL_SetWindowHitTest(window, hit_test_cb, NULL); + } else { + SDL_SetWindowHitTest(window, NULL, NULL); + } + + /* resize window for shadow padding */ + if (old_shadow != new_shadow) { + int w, h; + SDL_GetWindowSize(window, &w, &h); + SDL_SetWindowSize(window, w + (new_shadow - old_shadow) * 2, + h + (new_shadow - old_shadow) * 2); + } + return 0; } @@ -395,6 +468,8 @@ static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, { "key_modifier_down", f_key_modifier_down }, { "set_window_title", f_set_window_title }, { "set_window_mode", f_set_window_mode }, + { "minimize_window", f_minimize_window }, + { "configure_titlebar", f_configure_titlebar }, { "window_has_focus", f_window_has_focus }, { "show_error_dialog", f_show_error_dialog }, { "show_confirm_dialog", f_show_confirm_dialog }, { "list_dir", f_list_dir }, diff --git a/src/main.c b/src/main.c index 286c94d..723432e 100644 --- a/src/main.c +++ b/src/main.c @@ -155,6 +155,13 @@ int main(int argc, char **argv) { SDL_Init(SDL_INIT_VIDEO); SDL_EnableScreenSaver(); + /* This ladies and gents is why we looooove graphics API and window + * compositors - SDL does not yet provide a proper Wayland buffer + * handling (probably because Wayland is awsome :'| ). + * To have actual software rendered fake the window shadow when we + * take over the window native title bar we need to rely on X11 + + * OpenGL - merely for window composition - but we still blit into + * the buffer/surface. THIS WAS NOT FUN !! */ SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0"); SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1"); @@ -163,8 +170,18 @@ int main(int argc, char **argv) { int dw = dm ? dm->w : 1280; int dh = dm ? dm->h : 720; - window = SDL_CreateWindow("", dw * 4 / 5, dh * 4 / 5, - SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY); + SDL_WindowFlags window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN + | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_TRANSPARENT; + + /* This is the hack for X11 to setup OpenGL in visual selection to be able to + * fake window shadow since only this way we get the ability to use alpha + * channel */ + const char *video_driver = SDL_GetCurrentVideoDriver(); + if (video_driver && SDL_strcmp(video_driver, "x11") == 0) { + window_flags |= SDL_WINDOW_OPENGL; + } + + window = SDL_CreateWindow("", dw * 4 / 5, dh * 4 / 5, window_flags); if (!window) { fprintf(stderr, "Failed to create window: %s\n", SDL_GetError()); return EXIT_FAILURE; @@ -181,6 +198,18 @@ int main(int argc, char **argv) { luaL_openlibs(L); api_load_libs(L); + /* Create the window icon as a Lua userdata to use in the titlebar + * -- only on custom title bar */ + { +#include "../icon.inl" + RenImage *icon_img = + ren_new_image_from_rgba(64, 64, icon_rgba, icon_rgba_len); + RenImage **ud = lua_newuserdata(L, sizeof(*ud)); + *ud = icon_img; + luaL_setmetatable(L, API_TYPE_IMAGE); + lua_setglobal(L, "WINDOW_ICON"); + } + lua_newtable(L); for (int i = 0; i < argc; i++) { @@ -198,6 +227,8 @@ int main(int argc, char **argv) { lua_pushnumber(L, window_get_scale(window)); lua_setglobal(L, "SCALE"); + lua_pushnumber(L, window_get_pixel_density(window)); + char exename[2048]; get_exe_filename(argv[0], exename, sizeof(exename)); lua_pushstring(L, exename); diff --git a/src/rencache.c b/src/rencache.c index c13cc81..c7b39e3 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -3,15 +3,18 @@ #include "rencache.h" /* a cache over the software renderer -- all drawing operations are stored as -** commands when issued. At the end of the frame we write the commands to a grid -** of hash values, take the cells that have changed since the previous frame, -** merge them into dirty rectangles and redraw only those regions */ + * commands when issued. At the end of the frame we write the commands to a grid + * of hash values, take the cells that have changed since the previous frame, + * merge them into dirty rectangles and redraw only those regions */ #define CELLS_X 80 #define CELLS_Y 50 #define CELL_SIZE 96 #define COMMAND_BUF_SIZE (1024 * 512) +/* shadow max opacity */ +#define SHADOW_ALPHA 70 + enum { FREE_FONT, FREE_IMAGE, SET_CLIP, DRAW_TEXT, DRAW_RECT, DRAW_IMAGE }; typedef struct { @@ -35,6 +38,7 @@ static char command_buf[COMMAND_BUF_SIZE]; static size_t command_buf_idx; static RenRect screen_rect; static bool show_debug; +static int shadow_size; static inline int rc_min(int a, int b) { return a < b ? a : b; } @@ -52,14 +56,12 @@ static void hash(unsigned *h, const void *data, size_t size) { } -static inline int cell_idx(int x, int y) { - return x + y * CELLS_X; -} +static inline int cell_idx(int x, int y) { return x + y * CELLS_X; } static inline bool rects_overlap(RenRect a, RenRect b) { - return b.x + b.width >= a.x && b.x <= a.x + a.width - && b.y + b.height >= a.y && b.y <= a.y + a.height; + return b.x + b.width >= a.x && b.x <= a.x + a.width && b.y + b.height >= a.y + && b.y <= a.y + a.height; } @@ -81,8 +83,8 @@ static RenRect merge_rects(RenRect a, RenRect b) { } -static Command* push_command(int type, size_t size) { - Command *cmd = (Command*) (command_buf + command_buf_idx); +static Command *push_command(int type, size_t size) { + Command *cmd = (Command *) (command_buf + command_buf_idx); if (size > COMMAND_BUF_SIZE - command_buf_idx) { fprintf(stderr, "Warning: (" __FILE__ "): exhausted command buffer\n"); return NULL; @@ -98,17 +100,15 @@ static Command* push_command(int type, size_t size) { static bool next_command(Command **prev) { if (*prev == NULL) { - *prev = (Command*) command_buf; + *prev = (Command *) command_buf; } else { - *prev = (Command*) (((char*) *prev) + (*prev)->size); + *prev = (Command *) (((char *) *prev) + (*prev)->size); } - return *prev != ((Command*) (command_buf + command_buf_idx)); + return *prev != ((Command *) (command_buf + command_buf_idx)); } -void rencache_show_debug(bool enable) { - show_debug = enable; -} +void rencache_show_debug(bool enable) { show_debug = enable; } void rencache_free_font(RenFont *font) { @@ -123,13 +123,19 @@ void rencache_free_image(RenImage *image) { } +static inline RenRect offset_rect(RenRect r) { + return (RenRect) { r.x + shadow_size, r.y + shadow_size, r.width, r.height }; +} + + void rencache_set_clip_rect(RenRect rect) { Command *cmd = push_command(SET_CLIP, sizeof(Command)); - if (cmd) { cmd->rect = intersect_rects(rect, screen_rect); } + if (cmd) { cmd->rect = intersect_rects(offset_rect(rect), screen_rect); } } void rencache_draw_rect(RenRect rect, RenColor color) { + rect = offset_rect(rect); if (!rects_overlap(screen_rect, rect)) { return; } Command *cmd = push_command(DRAW_RECT, sizeof(Command)); if (cmd) { @@ -139,10 +145,12 @@ void rencache_draw_rect(RenRect rect, RenColor color) { } -int rencache_draw_text(RenFont *font, const char *text, int x, int y, RenColor color) { +int rencache_draw_text( + RenFont *font, const char *text, int x, int y, RenColor color) { + int orig_x = x; RenRect rect; - rect.x = x; - rect.y = y; + rect.x = x + shadow_size; + rect.y = y + shadow_size; rect.width = ren_get_font_width(font, text); rect.height = ren_get_font_height(font); @@ -158,11 +166,12 @@ int rencache_draw_text(RenFont *font, const char *text, int x, int y, RenColor c } } - return x + rect.width; + return orig_x + rect.width; } void rencache_draw_image(RenImage *image, RenRect rect) { + rect = offset_rect(rect); if (!rects_overlap(screen_rect, rect)) { return; } Command *cmd = push_command(DRAW_IMAGE, sizeof(Command)); if (cmd) { @@ -172,9 +181,7 @@ void rencache_draw_image(RenImage *image, RenRect rect) { } -void rencache_invalidate(void) { - memset(cells_prev, 0xff, sizeof(cells_buf1)); -} +void rencache_invalidate(void) { memset(cells_prev, 0xff, sizeof(cells_buf1)); } void rencache_begin_frame(void) { @@ -265,38 +272,56 @@ void rencache_end_frame(void) { cmd = NULL; while (next_command(&cmd)) { switch (cmd->type) { - case FREE_FONT: - case FREE_IMAGE: - break; - case SET_CLIP: - ren_set_clip_rect(intersect_rects(cmd->rect, r)); - break; - case DRAW_RECT: - ren_draw_rect(cmd->rect, cmd->color); - break; - case DRAW_TEXT: - ren_set_font_tab_width(cmd->font, cmd->tab_width); - ren_draw_text(cmd->font, cmd->text, cmd->rect.x, cmd->rect.y, cmd->color); - break; - case DRAW_IMAGE: - ren_draw_image_scaled(cmd->image, cmd->rect); - break; + case FREE_FONT: + case FREE_IMAGE: break; + case SET_CLIP: ren_set_clip_rect(intersect_rects(cmd->rect, r)); break; + case DRAW_RECT: ren_draw_rect(cmd->rect, cmd->color); break; + case DRAW_TEXT: + ren_set_font_tab_width(cmd->font, cmd->tab_width); + ren_draw_text( + cmd->font, cmd->text, cmd->rect.x, cmd->rect.y, cmd->color); + break; + case DRAW_IMAGE: ren_draw_image_scaled(cmd->image, cmd->rect); break; } } if (show_debug) { - RenColor color = { - (uint8_t) rand(), (uint8_t) rand(), (uint8_t) rand(), 50 - }; + RenColor color = { (uint8_t) rand(), (uint8_t) rand(), (uint8_t) rand(), + 50 }; ren_draw_rect(r, color); } } - /* update dirty rects */ - if (rect_count > 0) { - ren_update_rects(rect_buf, rect_count); + /* draw window shadow (this applies when title bar is being drawn by the + * engine and not by native ui). + * Draw quads with alpha increasing quadratically up to SHADOW_ALPHA + * against the actual editor content. */ + if (shadow_size > 0) { + int w = screen_rect.width; + int h = screen_rect.height; + int s = shadow_size; + ren_set_clip_rect(screen_rect); + for (int i = 0; i < s; i++) { + float t = (float) (i + 1) / s; + uint8_t a = (uint8_t) (SHADOW_ALPHA * t * t + 0.5f); + RenColor color = { 0, 0, 0, a }; + ren_fill_rect((RenRect) { i, i, w - i * 2, 1 }, color); + ren_fill_rect((RenRect) { i, h - i - 1, w - i * 2, 1 }, color); + ren_fill_rect((RenRect) { i, i + 1, 1, h - i * 2 - 2 }, color); + ren_fill_rect((RenRect) { w - i - 1, i + 1, 1, h - i * 2 - 2 }, color); + } + /* add shadow border to dirty rects so it gets pushed to screen */ + if (rect_count + 4 <= CELLS_X * CELLS_Y / 2) { + rect_buf[rect_count++] = (RenRect) { 0, 0, w, s }; + rect_buf[rect_count++] = (RenRect) { 0, h - s, w, s }; + rect_buf[rect_count++] = (RenRect) { 0, s, s, h - s * 2 }; + rect_buf[rect_count++] = (RenRect) { w - s, s, s, h - s * 2 }; + } } + /* update dirty rects */ + if (rect_count > 0) { ren_update_rects(rect_buf, rect_count); } + /* resources referenced by this frame can be released after drawing */ cmd = NULL; while (next_command(&cmd)) { @@ -313,3 +338,7 @@ void rencache_end_frame(void) { cells_prev = tmp; command_buf_idx = 0; } + +void rencache_set_shadow(int size) { shadow_size = size; } + +int rencache_get_shadow(void) { return shadow_size; } diff --git a/src/rencache.h b/src/rencache.h index cfc40d2..d4ebf50 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -9,10 +9,13 @@ void rencache_free_font(RenFont *font); void rencache_free_image(RenImage *image); void rencache_set_clip_rect(RenRect rect); void rencache_draw_rect(RenRect rect, RenColor color); -int rencache_draw_text(RenFont *font, const char *text, int x, int y, RenColor color); +int rencache_draw_text( + RenFont *font, const char *text, int x, int y, RenColor color); void rencache_draw_image(RenImage *image, RenRect rect); void rencache_invalidate(void); void rencache_begin_frame(void); void rencache_end_frame(void); +void rencache_set_shadow(int size); +int rencache_get_shadow(void); #endif diff --git a/src/renderer.c b/src/renderer.c index eca49f4..e246941 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -113,6 +113,17 @@ static void convert_rgba_to_ren_colors(RenColor *pixels, size_t count) { } +RenImage *ren_new_image_from_rgba( + int width, int height, const void *data, size_t data_len) { + size_t expected = (size_t) width * (size_t) height * 4; + if (data_len < expected) { return NULL; } + RenImage *image = ren_new_image(width, height); + memcpy(image->pixels, data, expected); + convert_rgba_to_ren_colors(image->pixels, (size_t) width * (size_t) height); + return image; +} + + static bool valid_image_size(int width, int height) { if (width <= 0 || height <= 0) { return false; } size_t maximum_pixels = (SIZE_MAX - sizeof(RenImage)) / sizeof(RenColor); @@ -127,8 +138,8 @@ static RenImage *load_raster_image(const uint8_t *data, size_t size) { } int width, height, channels; - stbi_uc *rgba = stbi_load_from_memory(data, (int) size, &width, &height, - &channels, STBI_rgb_alpha); + stbi_uc *rgba = stbi_load_from_memory( + data, (int) size, &width, &height, &channels, STBI_rgb_alpha); if (!rgba) { SDL_SetError("unsupported or invalid image: %s", stbi_failure_reason()); return NULL; @@ -151,8 +162,8 @@ static RenImage *load_raster_image(const uint8_t *data, size_t size) { static RenImage *load_svg_image(char *data) { NSVGimage *svg = nsvgParse(data, "px", 96.0f); if (!svg || svg->width <= 0 || svg->height <= 0 - || (double) svg->width > (double) INT_MAX - || (double) svg->height > (double) INT_MAX) { + || (double) svg->width > (double) INT_MAX + || (double) svg->height > (double) INT_MAX) { nsvgDelete(svg); SDL_SetError("unsupported or invalid SVG image"); return NULL; @@ -173,8 +184,8 @@ static RenImage *load_svg_image(char *data) { } RenImage *image = ren_new_image(width, height); - nsvgRasterize(rasterizer, svg, 0, 0, 1, (uint8_t *) image->pixels, - width, height, width * (int) sizeof(RenColor)); + nsvgRasterize(rasterizer, svg, 0, 0, 1, (uint8_t *) image->pixels, width, + height, width * (int) sizeof(RenColor)); nsvgDeleteRasterizer(rasterizer); nsvgDelete(svg); convert_rgba_to_ren_colors(image->pixels, (size_t) width * (size_t) height); @@ -193,9 +204,8 @@ RenImage *ren_load_image(const char *filename) { uint8_t *data = SDL_LoadFile(filename, &size); if (!data) { return NULL; } - RenImage *image = has_svg_extension(filename) - ? load_svg_image((char *) data) - : load_raster_image(data, size); + RenImage *image = has_svg_extension(filename) ? load_svg_image((char *) data) + : load_raster_image(data, size); SDL_free(data); return image; } @@ -391,6 +401,23 @@ void ren_draw_rect(RenRect rect, RenColor color) { } } +void ren_fill_rect(RenRect rect, RenColor color) { + int x1 = rect.x < clip.left ? clip.left : rect.x; + int y1 = rect.y < clip.top ? clip.top : rect.y; + int x2 = rect.x + rect.width; + int y2 = rect.y + rect.height; + x2 = x2 > clip.right ? clip.right : x2; + y2 = y2 > clip.bottom ? clip.bottom : y2; + if (x1 >= x2 || y1 >= y2) { return; } + + SDL_Surface *surf = SDL_GetWindowSurface(window); + RenColor *d = (RenColor *) surf->pixels; + d += x1 + y1 * surf->w; + int dr = surf->w - (x2 - x1); + + rect_draw_loop(color); +} + void ren_draw_image( RenImage *image, RenRect *sub, int x, int y, RenColor color) { @@ -451,11 +478,11 @@ void ren_draw_image_scaled(RenImage *image, RenRect rect) { int destination_skip = surface->w - (x2 - x1); for (int y = y1; y < y2; y++) { - int source_y = (int) (((int64_t) (y - rect.y) * image->height) - / rect.height); + int source_y = + (int) (((int64_t) (y - rect.y) * image->height) / rect.height); for (int x = x1; x < x2; x++) { - int source_x = (int) (((int64_t) (x - rect.x) * image->width) - / rect.width); + int source_x = + (int) (((int64_t) (x - rect.x) * image->width) / rect.width); RenColor source = image->pixels[source_x + source_y * image->width]; *destination = blend_pixel(*destination, source); destination++; diff --git a/src/renderer.h b/src/renderer.h index 4f3ad3d..0d6cee1 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -17,6 +17,7 @@ void ren_set_clip_rect(RenRect rect); void ren_get_size(int *x, int *y); RenImage* ren_new_image(int width, int height); +RenImage* ren_new_image_from_rgba(int width, int height, const void *data, size_t data_len); RenImage* ren_load_image(const char *filename); void ren_free_image(RenImage *image); int ren_get_image_width(RenImage *image); @@ -30,6 +31,7 @@ int ren_get_font_width(RenFont *font, const char *text); int ren_get_font_height(RenFont *font); void ren_draw_rect(RenRect rect, RenColor color); +void ren_fill_rect(RenRect rect, RenColor color); void ren_draw_image(RenImage *image, RenRect *sub, int x, int y, RenColor color); void ren_draw_image_scaled(RenImage *image, RenRect rect); int ren_draw_text(RenFont *font, const char *text, int x, int y, RenColor color); From 5c257570c251bf1fb32ef598a36d2ac5a162e880 Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 23:03:38 +0100 Subject: [PATCH 08/12] fix: windows and macOS builds --- CMakeLists.txt | 6 ++++++ src/api/renderer_image.c | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index eff75ae..04008c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,6 +201,12 @@ target_link_libraries(nsl PRIVATE if(APPLE) find_library(APPKIT_FRAMEWORK AppKit REQUIRED) target_link_libraries(nsl PRIVATE ${APPKIT_FRAMEWORK}) + + # SDL_cocoamouse.m always references GCMouse, but SDL only links + # GameController when SDL_JOYSTICK is on, which we disable. + # So enjoyable to support cross OS builds :'D + find_library(GAMECONTROLLER_FRAMEWORK GameController REQUIRED) + target_link_libraries(nsl PRIVATE ${GAMECONTROLLER_FRAMEWORK}) endif() if(UNIX) diff --git a/src/api/renderer_image.c b/src/api/renderer_image.c index 1bd97e5..207b27c 100644 --- a/src/api/renderer_image.c +++ b/src/api/renderer_image.c @@ -41,8 +41,8 @@ static int f_get_height(lua_State *L) { static int f_new_from_rgba(lua_State *L) { - int width = luaL_checkinteger(L, 1); - int height = luaL_checkinteger(L, 2); + int width = (int) luaL_checkinteger(L, 1); + int height = (int) luaL_checkinteger(L, 2); size_t data_len; const char *data = luaL_checklstring(L, 3, &data_len); RenImage **self = lua_newuserdata(L, sizeof(*self)); From e67caf9b0dce0ae943c3cc0372ca3ca2ddbd8dbd Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 23:07:30 +0100 Subject: [PATCH 09/12] fix: remove SDL3 from package since we now statically link it --- .github/workflows/build-and-release.yml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 4b043ed..6b30f4c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -49,8 +49,7 @@ jobs: libxkbcommon-dev \ libxrandr-dev \ libxss-dev \ - libxtst-dev \ - patchelf + libxtst-dev - name: Configure run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @@ -66,10 +65,6 @@ jobs: mkdir -p "$app_dir" cp build/nsl "$app_dir/" cp -a data "$app_dir/data" - sdl_library="$(find build -type f -name 'libSDL3.so*' -print -quit)" - test -n "$sdl_library" - cp -a "$(dirname "$sdl_library")"/libSDL3.so* "$app_dir/" - patchelf --set-rpath '$ORIGIN' "$app_dir/nsl" tar -C dist -czf nslite-linux.tar.gz nslite-linux - name: Package macOS artifact @@ -79,15 +74,9 @@ jobs: app_dir="dist/nslite-macos" mkdir -p "$app_dir" binary="$(find build -type f -name nsl -perm -111 -print -quit)" - sdl_library="$(find build -type f -name 'libSDL3*.dylib' -print -quit)" test -n "$binary" - test -n "$sdl_library" cp "$binary" "$app_dir/nsl" - cp "$sdl_library" "$app_dir/" cp -R data "$app_dir/data" - sdl_reference="$(otool -L "$app_dir/nsl" | awk '/SDL3/{print $1; exit}')" - install_name_tool -change "$sdl_reference" "@rpath/$(basename "$sdl_library")" "$app_dir/nsl" - install_name_tool -add_rpath '@loader_path' "$app_dir/nsl" tar -C dist -czf nslite-macos.tar.gz nslite-macos - name: Package Windows artifact @@ -97,10 +86,8 @@ jobs: $appDir = 'dist/nslite-windows' New-Item -ItemType Directory -Force -Path $appDir | Out-Null $binary = Get-ChildItem -Path build -Filter nsl.exe -Recurse | Select-Object -First 1 - $sdlLibrary = Get-ChildItem -Path build -Filter SDL3.dll -Recurse | Select-Object -First 1 - if (-not $binary -or -not $sdlLibrary) { throw 'Built executable or SDL3 runtime was not found.' } + if (-not $binary) { throw 'Built executable was not found.' } Copy-Item $binary.FullName $appDir - Copy-Item $sdlLibrary.FullName $appDir Copy-Item -Recurse data "$appDir/data" Compress-Archive -Path "$appDir/*" -DestinationPath nslite-windows.zip From aae4ace23e420205fe7416e2d5b61dddb3179f3e Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 23:13:31 +0100 Subject: [PATCH 10/12] chore: don't double compress -- drop archiving from upload artifact action --- .github/workflows/build-and-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 6b30f4c..05eb061 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -96,6 +96,7 @@ jobs: with: name: nslite-${{ matrix.name }} path: ${{ matrix.archive }} + archive: false if-no-files-found: error verify-tag-on-main: From 763c7ceb16b206d7507b1621b36bc4be7ed90f46 Mon Sep 17 00:00:00 2001 From: tacf Date: Mon, 17 Aug 2026 23:23:18 +0100 Subject: [PATCH 11/12] fix: i got bamboozled by MSFT docs -- clearly up-to-date -- more time in docs less in changing notepad pls --- .github/workflows/build-and-release.yml | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 05eb061..85ef029 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -21,13 +21,10 @@ jobs: include: - name: linux os: ubuntu-latest - archive: nslite-linux.tar.gz - name: macos os: macos-latest - archive: nslite-macos.tar.gz - name: windows os: windows-latest - archive: nslite-windows.zip steps: - uses: actions/checkout@v6 @@ -65,7 +62,6 @@ jobs: mkdir -p "$app_dir" cp build/nsl "$app_dir/" cp -a data "$app_dir/data" - tar -C dist -czf nslite-linux.tar.gz nslite-linux - name: Package macOS artifact if: matrix.name == 'macos' @@ -77,7 +73,6 @@ jobs: test -n "$binary" cp "$binary" "$app_dir/nsl" cp -R data "$app_dir/data" - tar -C dist -czf nslite-macos.tar.gz nslite-macos - name: Package Windows artifact if: matrix.name == 'windows' @@ -89,14 +84,12 @@ jobs: if (-not $binary) { throw 'Built executable was not found.' } Copy-Item $binary.FullName $appDir Copy-Item -Recurse data "$appDir/data" - Compress-Archive -Path "$appDir/*" -DestinationPath nslite-windows.zip - name: Upload artifact uses: actions/upload-artifact@v6 with: name: nslite-${{ matrix.name }} - path: ${{ matrix.archive }} - archive: false + path: dist/nslite-${{ matrix.name }} if-no-files-found: error verify-tag-on-main: @@ -133,20 +126,23 @@ jobs: uses: actions/download-artifact@v5 with: pattern: nslite-* - merge-multiple: true path: release-assets - - name: Add version to asset names + - name: Create release archives env: TAG: ${{ needs.verify-tag-on-main.outputs.tag }} run: | - mv release-assets/nslite-linux.tar.gz "release-assets/nslite-${TAG}-linux.tar.gz" - mv release-assets/nslite-macos.tar.gz "release-assets/nslite-${TAG}-macos.tar.gz" - mv release-assets/nslite-windows.zip "release-assets/nslite-${TAG}-windows.zip" + mkdir -p dist + # artifacts travel as zips, which carry no unix permissions, so the + # executable bit has to be put back before we repack for release + chmod +x release-assets/nslite-linux/nsl release-assets/nslite-macos/nsl + tar -C release-assets -czf "dist/nslite-${TAG}-linux.tar.gz" nslite-linux + tar -C release-assets -czf "dist/nslite-${TAG}-macos.tar.gz" nslite-macos + (cd release-assets && zip -qr "../dist/nslite-${TAG}-windows.zip" nslite-windows) - name: Create GitHub release env: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} TAG: ${{ needs.verify-tag-on-main.outputs.tag }} - run: gh release create "$TAG" release-assets/* --title "$TAG" --generate-notes + run: gh release create "$TAG" dist/* --title "$TAG" --generate-notes From 2c9958ba0cd3e94c2dd414ed6843c0ae3255b261 Mon Sep 17 00:00:00 2001 From: tacf Date: Tue, 18 Aug 2026 19:09:22 +0100 Subject: [PATCH 12/12] fix: maximized/fullscreen adjust window size when rendering own title bar --- Makefile | 4 ++-- src/api/system.c | 24 +++++++++++++++--------- src/rencache.c | 16 ++++++++++------ src/renderer.c | 6 ++++++ src/renderer.h | 2 ++ 5 files changed, 35 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 91130b2..8004cac 100644 --- a/Makefile +++ b/Makefile @@ -6,11 +6,11 @@ all: build build: @mkdir -p build - @cd build && cmake $(CMAKE_FLAGS) -DCMAKE_BUILD_TYPE=Debug .. && cmake --build . --parallel 2 + @cd build && cmake $(CMAKE_FLAGS) -DCMAKE_BUILD_TYPE=Debug .. && cmake --build . -j release: @mkdir -p build - @cd build && cmake $(CMAKE_FLAGS) -DCMAKE_BUILD_TYPE=Release .. && cmake --build . --parallel 2 + @cd build && cmake $(CMAKE_FLAGS) -DCMAKE_BUILD_TYPE=Release .. && cmake --build . -j clean: @rm -rf build diff --git a/src/api/system.c b/src/api/system.c index a8ae2bc..77a0ce5 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -11,6 +11,11 @@ extern SDL_Window *window; +static int g_titlebar_height = 0; +static int g_titlebar_drag_margin = 0; + +#define SHADOW_PX 8 + static const char *button_name(int button) { switch (button) { @@ -180,14 +185,20 @@ static int f_set_window_title(lua_State *L) { static const char *window_opts[] = { "normal", "maximized", "fullscreen", 0 }; enum { WIN_NORMAL, WIN_MAXIMIZED, WIN_FULLSCREEN }; +/* the shadow inset follows the window state (see rencache_get_shadow), so the + * fullscreen flag has to actually be cleared here -- SDL_RestoreWindow only + * undoes minimize/maximize */ static int f_set_window_mode(lua_State *L) { int n = luaL_checkoption(L, 1, "normal", window_opts); if (n == WIN_FULLSCREEN) { SDL_SetWindowFullscreen(window, true); - } else if (n == WIN_NORMAL) { - SDL_RestoreWindow(window); - } else if (n == WIN_MAXIMIZED) { - SDL_MaximizeWindow(window); + } else { + SDL_SetWindowFullscreen(window, false); + if (n == WIN_NORMAL) { + SDL_RestoreWindow(window); + } else if (n == WIN_MAXIMIZED) { + SDL_MaximizeWindow(window); + } } return 0; } @@ -200,11 +211,6 @@ static int f_minimize_window(lua_State *L) { } -static int g_titlebar_height = 0; -static int g_titlebar_drag_margin = 0; - -#define SHADOW_PX 8 - static SDL_HitTestResult SDLCALL hit_test_cb( SDL_Window *win, const SDL_Point *point, void *data) { (void) data; diff --git a/src/rencache.c b/src/rencache.c index c7b39e3..77d53d7 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -124,7 +124,8 @@ void rencache_free_image(RenImage *image) { static inline RenRect offset_rect(RenRect r) { - return (RenRect) { r.x + shadow_size, r.y + shadow_size, r.width, r.height }; + int s = rencache_get_shadow(); + return (RenRect) { r.x + s, r.y + s, r.width, r.height }; } @@ -149,8 +150,8 @@ int rencache_draw_text( RenFont *font, const char *text, int x, int y, RenColor color) { int orig_x = x; RenRect rect; - rect.x = x + shadow_size; - rect.y = y + shadow_size; + rect.x = x + rencache_get_shadow(); + rect.y = y + rencache_get_shadow(); rect.width = ren_get_font_width(font, text); rect.height = ren_get_font_height(font); @@ -296,10 +297,10 @@ void rencache_end_frame(void) { * engine and not by native ui). * Draw quads with alpha increasing quadratically up to SHADOW_ALPHA * against the actual editor content. */ - if (shadow_size > 0) { + if (rencache_get_shadow() > 0) { int w = screen_rect.width; int h = screen_rect.height; - int s = shadow_size; + int s = rencache_get_shadow(); ren_set_clip_rect(screen_rect); for (int i = 0; i < s; i++) { float t = (float) (i + 1) / s; @@ -341,4 +342,7 @@ void rencache_end_frame(void) { void rencache_set_shadow(int size) { shadow_size = size; } -int rencache_get_shadow(void) { return shadow_size; } +/* maximized/fullscreen windows supress fake shadow */ +int rencache_get_shadow(void) { + return ren_window_is_maximized_or_full() ? 0 : shadow_size; +} diff --git a/src/renderer.c b/src/renderer.c index e246941..ae00cab 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -92,6 +92,12 @@ void ren_get_size(int *x, int *y) { } +bool ren_window_is_maximized_or_full(void) { + SDL_WindowFlags flags = SDL_GetWindowFlags(window); + return (flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) != 0; +} + + RenImage *ren_new_image(int width, int height) { assert(width > 0 && height > 0); size_t pixel_count = (size_t) width * (size_t) height; diff --git a/src/renderer.h b/src/renderer.h index 0d6cee1..3eba30e 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -2,6 +2,7 @@ #define RENDERER_H #include +#include #include typedef struct RenImage RenImage; @@ -15,6 +16,7 @@ void ren_init(SDL_Window *win); void ren_update_rects(RenRect *rects, int count); void ren_set_clip_rect(RenRect rect); void ren_get_size(int *x, int *y); +bool ren_window_is_maximized_or_full(void); RenImage* ren_new_image(int width, int height); RenImage* ren_new_image_from_rgba(int width, int height, const void *data, size_t data_len);