From b65292519961f1dc5173ed0b8ac9acf9fb504de6 Mon Sep 17 00:00:00 2001 From: 5cript Date: Thu, 15 Oct 2026 23:58:58 +0200 Subject: [PATCH 1/6] Dropped the hover transitions of the file grid icons and shrank their shadows, WebKitGTK paints without compositing and repainted the whole grid on every transition frame --- nui-file-explorer/styles/file_grid.css | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/nui-file-explorer/styles/file_grid.css b/nui-file-explorer/styles/file_grid.css index f178bb45..b6b0aa14 100644 --- a/nui-file-explorer/styles/file_grid.css +++ b/nui-file-explorer/styles/file_grid.css @@ -212,8 +212,8 @@ overflow: hidden; isolation: isolate; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35); - transition: transform 120ms ease, box-shadow 120ms ease; + /* WebKitGTK paints without compositing: blurry shadows and hover transitions repaint the whole grid */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.35); &[data-type="Symbolic Link"] { color: var(--nui-file-grid-symbolic-link-color, rgb(0, 242, 255)); @@ -261,8 +261,6 @@ linear-gradient(145deg, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0.05)); - - transition: background 120ms ease; } /* ── Specular highlight border ── */ @@ -277,7 +275,6 @@ inset 2px 2px 0px -1px rgba(255, 255, 255, 0.75), inset 0 0 4px 1px rgba(255, 255, 255, 0.45); pointer-events: none; - transition: box-shadow 120ms ease, border-color 120ms ease; } /* ── Content above pseudo layers ── */ @@ -288,7 +285,7 @@ .nui-file-grid-item-icons.is-muted { opacity: 0.55; - box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45); + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.45); filter: saturate(70%) brightness(85%); } @@ -343,7 +340,7 @@ .nui-file-grid-item-icons:hover { transform: translateY(-2px); - box-shadow: 0 16px 32px rgba(0, 0, 0, 0.45); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.45); } .nui-file-grid-item-icons:hover::before { From 21c5ef025804bac4472e6f3b678c4b024044eb91 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 16 Oct 2026 00:04:56 +0200 Subject: [PATCH 2/6] Pre-rendered the file grid background gradients into images for the dark and light theme, WebKitGTK painted the seven radial gradients on the CPU for every repaint of the grid and of anything above it --- _cmake/copy_styles_to_build_dir.cmake | 8 ++ frontend/source/frontend/CMakeLists.txt | 2 + nui-file-explorer/styles/file_grid.css | 16 +--- .../styles/file_grid_background.png | Bin 0 -> 13588 bytes .../styles/file_grid_background_light.png | Bin 0 -> 11671 bytes scripts/render_file_grid_background.py | 71 ++++++++++++++++++ static/styles/main.css | 1 + themes/Plain.css | 2 +- 8 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 nui-file-explorer/styles/file_grid_background.png create mode 100644 nui-file-explorer/styles/file_grid_background_light.png create mode 100755 scripts/render_file_grid_background.py diff --git a/_cmake/copy_styles_to_build_dir.cmake b/_cmake/copy_styles_to_build_dir.cmake index 90e1bf04..738835c0 100644 --- a/_cmake/copy_styles_to_build_dir.cmake +++ b/_cmake/copy_styles_to_build_dir.cmake @@ -2,6 +2,8 @@ if (NOT OMIT_FRONTEND_BUILD) add_custom_command( OUTPUT "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid.css" + "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid_background.png" + "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid_background_light.png" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/button.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/carousel.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/color_picker.css" @@ -27,6 +29,8 @@ if (NOT OMIT_FRONTEND_BUILD) COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/styles/5cript-nui-components" COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/themes" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid.css" "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid.css" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid_background.png" "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid_background.png" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid_background_light.png" "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid_background_light.png" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/button.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/button.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/carousel.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/carousel.css" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/color_picker.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/color_picker.css" @@ -50,6 +54,8 @@ if (NOT OMIT_FRONTEND_BUILD) COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/themes/light.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/themes/light.css" DEPENDS "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid.css" + "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid_background.png" + "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid_background_light.png" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/button.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/carousel.css" "${CMAKE_SOURCE_DIR}/dependencies/5cript-nui-components/styles/color_picker.css" @@ -78,6 +84,8 @@ if (NOT OMIT_FRONTEND_BUILD) ALL DEPENDS "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid.css" + "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid_background.png" + "${CMAKE_BINARY_DIR}/styles/nui-file-explorer/file_grid_background_light.png" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/button.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/carousel.css" "${CMAKE_BINARY_DIR}/styles/5cript-nui-components/color_picker.css" diff --git a/frontend/source/frontend/CMakeLists.txt b/frontend/source/frontend/CMakeLists.txt index be3e9c10..2e1da979 100644 --- a/frontend/source/frontend/CMakeLists.txt +++ b/frontend/source/frontend/CMakeLists.txt @@ -247,6 +247,8 @@ nui_prepare_emscripten_target( OBSERVED_BUNDLE_FILES "${CMAKE_SOURCE_DIR}/static/index.html" "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid.css" + "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid_background.png" + "${CMAKE_SOURCE_DIR}/nui-file-explorer/styles/file_grid_background_light.png" "${CMAKE_SOURCE_DIR}/static/source/content_panels/file_explorer.ts" "${CMAKE_SOURCE_DIR}/static/source/content_panels/terminal.ts" "${CMAKE_SOURCE_DIR}/static/source/content_panel_manager.ts" diff --git a/nui-file-explorer/styles/file_grid.css b/nui-file-explorer/styles/file_grid.css index b6b0aa14..a9baba53 100644 --- a/nui-file-explorer/styles/file_grid.css +++ b/nui-file-explorer/styles/file_grid.css @@ -48,21 +48,13 @@ } .nui-file-grid-background { - /* Makes glass work */ - --background-image: - radial-gradient(at 56% 8%, hsla(323, 64%, 77%, 1) 0px, transparent 50%), - radial-gradient(at 88% 7%, hsla(186, 94%, 76%, 1) 0px, transparent 50%), - radial-gradient(at 67% 1%, hsla(321, 80%, 79%, 1) 0px, transparent 50%), - radial-gradient(at 60% 69%, hsla(39, 75%, 79%, 1) 0px, transparent 50%), - radial-gradient(at 22% 33%, hsla(314, 83%, 65%, 1) 0px, transparent 50%), - radial-gradient(at 7% 91%, hsla(200, 78%, 70%, 1) 0px, transparent 50%), - radial-gradient(at 19% 86%, hsla(137, 62%, 74%, 1) 0px, transparent 50%); - - background-image: var(--background-image); + /* Makes glass work. Radial gradients multiplied over the background color, pre-rendered by + scripts/render_file_grid_background.py because WebKitGTK paints live gradients slowly */ + background-image: var(--background-image, url("data-url:./file_grid_background.png")); + background-size: 100% 100%; /* Darken overlay to improve contrast (doesn't affect child elements) */ background-color: rgba(0, 0, 0, 0.55); - background-blend-mode: multiply; } .nui-file-grid-content:first-child { diff --git a/nui-file-explorer/styles/file_grid_background.png b/nui-file-explorer/styles/file_grid_background.png new file mode 100644 index 0000000000000000000000000000000000000000..b3de0f507ed6a1b238a9eb65c9cd6275061dd85c GIT binary patch literal 13588 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 literal 0 HcmV?d00001 diff --git a/scripts/render_file_grid_background.py b/scripts/render_file_grid_background.py new file mode 100755 index 00000000..6a377dac --- /dev/null +++ b/scripts/render_file_grid_background.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# Renders the file grid background images into nui-file-explorer/styles. +# Seven radial gradients multiplied over the grid background color, following CSS Images 3 (farthest-corner +# ellipses, premultiplied interpolation) and Compositing 1 (separable blend, then source-over) in sRGB. +# The image holds only what the gradients add, drawn over that same background color it reproduces the blend. +# WebKitGTK paints the live gradients on the CPU on every repaint, a stretched image is far cheaper. +# Usage: render_file_grid_background.py [width] [height] +import colorsys +import sys +from pathlib import Path + +import numpy as np +from PIL import Image + +styles = Path(__file__).resolve().parent.parent / "nui-file-explorer/styles" +width = int(sys.argv[1]) if len(sys.argv) > 1 else 160 +height = int(sys.argv[2]) if len(sys.argv) > 2 else 128 + +# (x %, y %, hue, saturation %, lightness %), first entry is the top layer +spots = [ + (56, 8, 323, 64, 77), + (88, 7, 186, 94, 76), + (67, 1, 321, 80, 79), + (60, 69, 39, 75, 79), + (22, 33, 314, 83, 65), + (7, 91, 200, 78, 70), + (19, 86, 137, 62, 74), +] + +# file name -> background color (r, g, b, a) of .nui-file-grid-background in that theme +variants = { + "file_grid_background.png": (0, 0, 0, 0.55), + "file_grid_background_light.png": (224 / 255, 224 / 255, 224 / 255, 1), +} + + +def blendedGradients(backdropColor, backdropAlpha): + rows, columns = np.mgrid[0:height, 0:width].astype(np.float64) + 0.5 + premultiplied = np.broadcast_to(np.array(backdropColor) * backdropAlpha, (height, width, 3)).copy() + alpha = np.full((height, width), backdropAlpha) + for spotX, spotY, hue, saturation, lightness in reversed(spots): + source = np.array(colorsys.hls_to_rgb(hue / 360, lightness / 100, saturation / 100)) + centerX = spotX / 100 * width + centerY = spotY / 100 * height + radiusX = max(centerX, width - centerX) * np.sqrt(2) + radiusY = max(centerY, height - centerY) * np.sqrt(2) + distance = np.sqrt(((columns - centerX) / radiusX) ** 2 + ((rows - centerY) / radiusY) ** 2) + sourceAlpha = np.clip(1 - distance / 0.5, 0, 1)[..., None] + + layerAlpha = alpha[..., None] + backdrop = premultiplied / np.maximum(layerAlpha, 1e-9) + blended = (1 - layerAlpha) * source + layerAlpha * source * backdrop + premultiplied = sourceAlpha * blended + (1 - sourceAlpha) * premultiplied + alpha = sourceAlpha[..., 0] + alpha * (1 - sourceAlpha[..., 0]) + return premultiplied, alpha + + +for fileName, (red, green, blue, backdropAlpha) in variants.items(): + backdropColor = np.array([red, green, blue]) + premultiplied, alpha = blendedGradients(backdropColor, backdropAlpha) + if backdropAlpha >= 1: + imageAlpha = np.ones_like(alpha) + imagePremultiplied = premultiplied + else: + # Solve image over backdrop == blended result + imageAlpha = np.clip((alpha - backdropAlpha) / (1 - backdropAlpha), 0, 1) + imagePremultiplied = premultiplied - (1 - imageAlpha[..., None]) * backdropAlpha * backdropColor + color = np.clip(imagePremultiplied / np.maximum(imageAlpha[..., None], 1e-9), 0, 1) + pixels = np.round(np.dstack([color, imageAlpha]) * 255).astype(np.uint8) + Image.fromarray(pixels, "RGBA").save(styles / fileName, optimize=True) + print(f"{styles / fileName} {width}x{height}") diff --git a/static/styles/main.css b/static/styles/main.css index abd61686..acd519cc 100644 --- a/static/styles/main.css +++ b/static/styles/main.css @@ -224,6 +224,7 @@ & .nui-file-grid-background { background-color: var(--background-color); + background-image: var(--background-image, url("data-url:../../../styles/nui-file-explorer/file_grid_background_light.png")); } & .nui-file-grid-item-icons:is(.selected)::after { diff --git a/themes/Plain.css b/themes/Plain.css index e2320a2b..647ecf87 100644 --- a/themes/Plain.css +++ b/themes/Plain.css @@ -7,5 +7,5 @@ } .nui-file-grid-background { - --background-image: unset !important; + --background-image: none !important; } \ No newline at end of file From ab34aba4307bdd1beb75dd15e89e812db12d8743 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 16 Oct 2026 00:07:52 +0200 Subject: [PATCH 3/6] Skipped painting the session area while settings, licenses or the notification log cover it, WebKitGTK repainted the file grid underneath for every hover in the overlay --- static/styles/main_page.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/styles/main_page.css b/static/styles/main_page.css index 2511671e..f59a1dc3 100644 --- a/static/styles/main_page.css +++ b/static/styles/main_page.css @@ -12,4 +12,10 @@ .main-page-wrap { width: 100%; height: 100%; -} \ No newline at end of file +} +/* WebKitGTK paints without compositing and repaints whatever lies under an overlay repaint, skip the covered session area */ +.settings-page-background-blocker[style*="display: flex"] ~ .main-page .session-area, +.licenses-page-background-blocker[style*="display: flex"] ~ .main-page .session-area, +.notification-log-background-blocker[style*="display: flex"] ~ .main-page .session-area { + content-visibility: hidden; +} From a4115ad10e343ac02f91f185d55c146c0e608d98 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 16 Oct 2026 00:26:29 +0200 Subject: [PATCH 4/6] Named the program for GLib on Linux, nui-sftp or the flatpak app id, GTK 4 does not take it from argv and left the X11 WM_CLASS and the Wayland app id empty, and WebKit 6.0 in a flatpak needs the app id or the portal refuses its web process --- backend/source/backend/main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/source/backend/main.cpp b/backend/source/backend/main.cpp index f10ebffe..3707f3ed 100644 --- a/backend/source/backend/main.cpp +++ b/backend/source/backend/main.cpp @@ -8,6 +8,8 @@ #else # include # include + +# include #endif #include @@ -476,6 +478,10 @@ int main(int const argc, char const* const* argv) #ifdef __linux__ setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", 0); + // GTK 4 does not take it from argv like GTK 3, it names the X11 WM_CLASS and the Wayland app id. + // In a flatpak WebKit has to use the app id, the portal refuses to spawn its sandboxed web process otherwise. + const char* const flatpakApplicationId = std::getenv("FLATPAK_ID"); + g_set_prgname(flatpakApplicationId != nullptr && *flatpakApplicationId != '\0' ? flatpakApplicationId : "nui-sftp"); #endif ssh_init(); From dbf5488c69602083fb8a6c52c62e3d8132071b1c Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 16 Oct 2026 00:26:29 +0200 Subject: [PATCH 5/6] Looked up WebKitGTK once for Nui and webview, preferring webkitgtk-6.0 and falling back to webkit2gtk-4.1, each picked its own API and the binary linked GTK 3 and GTK 4 at once --- CMakeLists.txt | 49 +++++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a88e571b..2361e373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,24 +24,6 @@ option(FETCH_EFSW "Fetch efsw" ON) option(NUI_SFT_ENABLE_LANGUAGE_TOOLING "Enable building language tooling, which includes a command line tool for checking language files" OFF) option(TAR_ARCHIVE_HAS_XZ "Enable xz (liblzma) compression in the tar-archive library" ON) -if (OFFLINE_BUILD) - include("${CMAKE_CURRENT_LIST_DIR}/_cmake/offline_build.cmake") -endif() - -if (OMIT_FRONTEND_BUILD) - set(NUI_OMIT_FRONTEND ON CACHE BOOL "Do not build the frontend, sometimes useful for environments where emscripten is hard to use" FORCE) -endif() - -if (NOT EMSCRIPTEN) - set(BACKEND_BUILD_TYPE "${CMAKE_BUILD_TYPE}") -endif() -configure_file( - "${CMAKE_CURRENT_LIST_DIR}/_cmake/configured_files/build_environment.hpp.in" - "${CMAKE_BINARY_DIR}/include/build_environment.hpp" -) - -include (${CMAKE_CURRENT_LIST_DIR}/_cmake/common_options.cmake) - # Webkit from custom build if (NOT EMSCRIPTEN) if (WEBKIT_PATH) @@ -62,6 +44,37 @@ if (NOT EMSCRIPTEN) endif() endif() +# Nui and webview both look up WebKitGTK, with different preferences they linked GTK 3 and GTK 4 at once. +# One API for both: webkitgtk-6.0 (GTK 4) where it is installed, otherwise webkit2gtk-4.1 (GTK 3). +if (NOT EMSCRIPTEN AND UNIX AND NOT APPLE) + find_package(PkgConfig REQUIRED) + if (WEBKIT_PATH) + pkg_search_module(webkit REQUIRED IMPORTED_TARGET ${WEBKIT_PACKAGE_NAME}) + else() + pkg_search_module(webkit REQUIRED IMPORTED_TARGET webkitgtk-6.0 webkit2gtk-4.1 webkit2gtk-4.0) + endif() + set(WEBVIEW_WEBKITGTK_MODULE_NAME "${webkit_MODULE_NAME}") + message(STATUS "WebKitGTK API: ${webkit_MODULE_NAME}") +endif() + +if (OFFLINE_BUILD) + include("${CMAKE_CURRENT_LIST_DIR}/_cmake/offline_build.cmake") +endif() + +if (OMIT_FRONTEND_BUILD) + set(NUI_OMIT_FRONTEND ON CACHE BOOL "Do not build the frontend, sometimes useful for environments where emscripten is hard to use" FORCE) +endif() + +if (NOT EMSCRIPTEN) + set(BACKEND_BUILD_TYPE "${CMAKE_BUILD_TYPE}") +endif() +configure_file( + "${CMAKE_CURRENT_LIST_DIR}/_cmake/configured_files/build_environment.hpp.in" + "${CMAKE_BINARY_DIR}/include/build_environment.hpp" +) + +include (${CMAKE_CURRENT_LIST_DIR}/_cmake/common_options.cmake) + # Nui Dependency add_subdirectory("${CMAKE_SOURCE_DIR}/dependencies/Nui" EXCLUDE_FROM_ALL) From 7a6068b6055eb8cf279b5f9c35caa33bc11838d4 Mon Sep 17 00:00:00 2001 From: 5cript Date: Fri, 16 Oct 2026 01:50:44 +0200 Subject: [PATCH 6/6] Declared the Parcel plugins behind data-url imports, Parcel only installs missing plugins outside CI and the CI frontend build failed --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index da61aefa..4b20e5df 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "webpage:serve": "npm --prefix webpage run serve" }, "devDependencies": { + "@parcel/optimizer-data-url": "^2.13.0", + "@parcel/transformer-inline-string": "^2.13.0", "cross-env": "^7.0.3", "dotenv": "^16.4.5", "less": "^4.2.2",