diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 4b043ed..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 @@ -49,8 +46,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,11 +62,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 if: matrix.name == 'macos' @@ -79,16 +70,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 if: matrix.name == 'windows' @@ -97,18 +81,15 @@ 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 - name: Upload artifact uses: actions/upload-artifact@v6 with: name: nslite-${{ matrix.name }} - path: ${{ matrix.archive }} + path: dist/nslite-${{ matrix.name }} if-no-files-found: error verify-tag-on-main: @@ -145,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 diff --git a/CMakeLists.txt b/CMakeLists.txt index bce31b9..04008c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,18 +38,35 @@ 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) + # 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) + set(SDL_LIBUDEV OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(SDL3) set(BUILD_STATIC_LIBS ON CACHE BOOL "" FORCE) @@ -60,32 +77,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) @@ -184,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/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/data/core/commands/core.lua b/data/core/commands/core.lua index 080f427..74fdd82 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -1,11 +1,16 @@ 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 LogView = require "core.logview" +local style = require "core.style" +local LogView = require "core.presentations.log" +local TitleBarView = require "core.presentations.titlebar" +local renderer = require "renderer" local fullscreen = false +local debug_enabled = false command.add(nil, { ["core:quit"] = function() @@ -88,6 +93,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 @@ -98,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/commands/root.lua b/data/core/commands/root.lua index 4223b16..092f6f5 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) + node:del_child(idx) + node:add_child(core.active_view, idx - 1) 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 + node:del_child(idx) + node:add_child(core.active_view, idx + 1) 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/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/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/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/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/keymap.lua b/data/core/keymap.lua index 1226145..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 @@ -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/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/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 b8de4a8..4740aef 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -1,52 +1,23 @@ 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 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() -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 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 +25,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 +35,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 +43,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 +56,46 @@ 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 if dir == "up" or dir == "left" then - self.a, self.b = self.b, self.a + self:add_child(other) + self:add_child(child) + else + self:add_child(child) + self:add_child(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 + self:del_child(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 +105,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 + self:del_child(1) end - table.insert(self.views, view) + self:add_child(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 +126,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 +165,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 +182,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 +204,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 +211,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 +229,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 +262,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 +272,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 +296,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 +322,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 +352,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 +385,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 +401,6 @@ function Node:draw() end end - - local RootView = View:extend() function RootView:new() @@ -479,14 +409,15 @@ 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, ...) 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 +426,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 +434,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 +446,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 +458,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 +469,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 @@ -557,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 @@ -568,9 +501,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,16 +514,17 @@ 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) self.mouse.x, self.mouse.y = x, y local fv = self:get_active_floating_view() @@ -600,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 @@ -614,14 +555,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 +598,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,14 +616,24 @@ 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) + 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 @@ -691,8 +641,10 @@ function RootView:update() end 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) @@ -710,5 +662,4 @@ function RootView:draw() end end - return RootView 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 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..dfcb08f --- /dev/null +++ b/data/core/utils/tree.lua @@ -0,0 +1,66 @@ +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(child, pos) + local nodes = self.nodes or {} + 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 + +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() diff --git a/data/fonts/icons.ttf b/data/fonts/icons.ttf index d74fe73..185e687 100644 Binary files a/data/fonts/icons.ttf and b/data/fonts/icons.ttf differ 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 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..207b27c 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 = (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)); + 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..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) { @@ -33,12 +38,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 +53,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 +69,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 +92,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 +101,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; @@ -180,11 +185,85 @@ 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); - 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 { + SDL_SetWindowFullscreen(window, false); + 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 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 +474,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..77d53d7 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,20 @@ void rencache_free_image(RenImage *image) { } +static inline RenRect offset_rect(RenRect r) { + int s = rencache_get_shadow(); + return (RenRect) { r.x + s, r.y + s, 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 +146,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 + rencache_get_shadow(); + rect.y = y + rencache_get_shadow(); rect.width = ren_get_font_width(font, text); rect.height = ren_get_font_height(font); @@ -158,11 +167,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 +182,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 +273,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 (rencache_get_shadow() > 0) { + int w = screen_rect.width; + int h = screen_rect.height; + int s = rencache_get_shadow(); + 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 +339,10 @@ void rencache_end_frame(void) { cells_prev = tmp; command_buf_idx = 0; } + +void rencache_set_shadow(int size) { shadow_size = 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/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..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; @@ -113,6 +119,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 +144,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 +168,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 +190,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 +210,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 +407,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 +484,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..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,8 +16,10 @@ 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); RenImage* ren_load_image(const char *filename); void ren_free_image(RenImage *image); int ren_get_image_width(RenImage *image); @@ -30,6 +33,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);