Repository navigation
feat(app): close upstream lifecycle, metadata, and content composition gaps - #2
Open
jinzhongjia wants to merge 36 commits into
Open
jinzhongjia wants to merge 36 commits into
jinzhongjia wants to merge 36 commits into
Conversation
Running.wait() now evaluates every window on its own. A backend close ends only that window, other disconnects get the reconnect grace from the latest disconnect, and a new client clears the close intent. Add App.Options.startup_timeout (15 s by default) for first-connection waiting, restarted by open/openWithBrowser and extended by page or bridge requests, plus Running.requestExit() as the webui_exit equivalent.
Call now carries the matched binding name and whether it came from an explicit call or a DOM click. Call and Event expose the Cookie header of the client's WebSocket upgrade, like upstream webui_event_t.cookies. The header is copied per connection and bounded by the new Limits.max_cookie_size; larger upgrades are answered with 431.
favicon.ico and favicon.svg now resolve the custom icon, then a readable directory file, then the built-in default SVG, with favicon.ico redirecting to favicon.svg like upstream. The origin root serves the same default for pages that declare no icon link.
Content.site combines optional embedded HTML, a resource handler, a root folder, and an entry file, resolved in upstream order: handler, virtual-directory index probing, root HTML, folder files, then the default favicon or 404. A handler declines a path with an empty 404. The entry redirects the root like webui_show(window, "page.html"). Plain custom content gains the same virtual-index probing, and the directory branch is shared with site folders.
Window.installContent swaps the served content for later requests and leaves connected pages in place, like changing the root folder or file handler of a shown upstream window. External URLs change the page origin, so either side being external returns NavigationRequired.
Rejected clients may already be disconnected by the server, which macOS reports as an unconnected socket on shutdown. An unbound call is answered without entering the event queue, so the integration test now waits for the queued connect and click handlers instead of reading their flags.
The server acknowledges CHECK_TOKEN before registering the client, so tests that just authenticated poll for the registration instead of reading isShown immediately.
Monitors now live in a per-window group so one window's monitor can be cancelled without stopping the others. They are spawned with concurrent before the server starts, so an unavailable worker fails start with an error instead of running the endless poll loop inline.
createWindow now serves a window at once when called after start, with fresh credentials, its folder, and its own monitor. destroyWindow mirrors webui_destroy: it unregisters the window immediately, notifies connected pages, closes later transports with 1001, and cancels the window's handlers, monitor, and managed browser in the background. Window state is reference counted by the app list, upgrade records, in-flight requests, and deferred replies, and the window registry is guarded by a read-write lock, so a destroyed window is freed only after its last user finishes. Running.stop completes any pending cleanup. Client.window exposes the owning window so handlers can destroy it.
…dlers destroyWindow spawned the reaper before sending the close notification. When called from one of the window's own handlers, the reaper could cancel that handler mid-broadcast, the write failed with error.Canceled, and the transport was closed without the notification. The reaper now starts after the broadcast, still under the registry lock so it cannot race stop().
The loop slept 30 times for 100 ms. On a slow runner that exceeded the five-second authentication deadline, so the server closed the socket under the pings and the test failed with a write error. Ping for three seconds of wall time and stop pinging once the socket is closed.
Add upstream's toolbar-hiding userChrome.css and a user.js that enables it and turns off the default-browser check, the close warning, tabs in the title bar, and the first-run pages. user.js is rewritten on every call and always sets browser.display.document_color_use, so a changed high-contrast setting takes effect even after Firefox stored the old value in prefs.js.
Firefox windows without a caller profile now get a per-window generated profile like Chromium windows, prepared with the app-mode settings before each launch. high_contrast = false is honoured through the profile; a caller profile is never modified, so it still returns UnsupportedBrowserHighContrast. Snap Firefox cannot see the host /tmp. When /snap/bin/firefox exists, the Firefox root moves into the snap's user directory, found through the passwd home as snapd does, because Zig 0.16 exposes no process environment here. Profile lookups therefore take an Io, and launch receives the generated profile separately from the caller's controls.
Chrome and Chromium both install and register chrome.exe. Like upstream, a folder holding Google's initial_preferences or legacy master_preferences file is Google Chrome and any other chrome.exe is Chromium. Every chrome.exe candidate from PATH and App Paths is classified, so a Chromium chrome.exe is no longer reported as Chrome and registered Chromium is discovered.
Discovery now also checks ~/Applications and then asks Spotlight for the browser's bundle identifier, which finds bundles wherever LaunchServices registered them. Upstream uses open -R -a for this lookup, which reveals the application in Finder; mdfind has no side effects and a missing or disabled index only yields no result.
COM vtable entries and GetProcAddress results are byte-aligned opaque pointers, while function pointers require target alignment on aarch64. Assert that alignment with @aligncast so aarch64-windows compiles.
The WebMessageReceived close handler becomes an instance of a generic COM event handler parameterized by interface ID and callback, so further ICoreWebView2 events reuse the same reference counting and owner detachment instead of duplicating them.
Like upstream, every non-empty page title now replaces the native host title: GTK listens to notify::title, WebView2 to DocumentTitleChanged, and Cocoa observes WKWebView.title through KVO, which also reports document.title changes after load that upstream's didFinishNavigation hook misses. Options.title is the initial title and setTitle applies at once until the page reports another one. An empty title keeps the host title; WebView2 reports its own default for untitled documents. Options.follow_page_title and Window.setFollowPageTitle let hosts keep titles under their own control, and Window.title reads the current host title. The native smoke verifies following, setTitle, empty titles, and a non-following window on every platform.
Environment, controller, and document-script creation were bounded to 15 s. Hosted Windows runners take about 7 s to create two windows and occasionally exceed 15 s while spawning the browser processes, failing with NativeInitializationTimeout although the runtime is healthy. Raise the bound to 60 s; it still turns a runtime that never answers into an explicit error.
WebKitGTK has no CSS app-region support. As upstream WebUI does on Linux, a document-start script finds the nearest `--webui-app-region` value and, once the primary button moves over a `drag` area, posts one request on a dedicated script-message channel. The host starts a window-manager move only while the primary button is actually held, so a page cannot move the window by posting on its own. Frameless resizable windows also gain upstream's 6 px edge band: pressing it starts a window-manager resize from the matching edge or corner, and hovering shows the matching resize cursor, restoring the page cursor when leaving the band.
…ames Like upstream WebUI (PR #718), enable ICoreWebView2Settings9 non-client region support so CSS `app-region: drag` areas act as the host caption. Runtimes without Settings9 keep working and record that drag regions are unavailable. Resizable frameless windows keep a WS_THICKFRAME sizing border as upstream does; kiosk windows stay plain popups.
Match upstream WebUI's Cocoa adapter: frameless windows are movable by their background. The setting follows frameless changes at creation, style updates, and kiosk transitions; kiosk windows are never movable.
native.Window.dragRegion() returns the backend's drag model: the `--webui-app-region` property on WebKitGTK, CSS `app-region` on WebView2, background movement on Cocoa, or none on WebView2 runtimes without Settings9, so unsupported drag regions are reported instead of being silently ignored.
The native smoke makes the first window frameless, declares a page-wide drag region, and synthesizes pointer input: xdotool under X11 and mouse_event on Windows, from a worker so the Windows modal move and size loops can run. It checks that a drag moves the window, a right-edge drag widens it, and on GTK that a forged drag message without a held button does not move it. macOS, which has no input synthesis without Accessibility permission, verifies the background-movement setting. CI installs xdotool and passes --require-input so missing input tools fail instead of skipping.
…n gap Describe each backend's drag-region model, the GTK held-button rule, edge resizing, and portable CSS for pages, and move native interaction from the open semantic gaps to the closed table with its smoke and unit evidence.
Add NavigationRequest, NavigationKind, and NavigationHandler with Options.navigation_handler and Window.setNavigationHandler. The facade wraps the handler in a callback scope that rejects reentrant deinit. WebKitGTK answers decide-policy like upstream's policy handler, independent of any bridge connection: the host's own loads are exempt, every redirect hop is reported, and an unreadable request is cancelled.
Make the delegate the navigation delegate and answer decidePolicyForNavigationAction exactly once on every path. The host flag is consumed only by a main-frame decision, and server redirect hops reach the handler because WKWebView exposes no public redirect flag.
Subscribe NavigationStarting and FrameNavigationStarting and cancel through put_Cancel when the handler declines. Only the top-level event can consume the host flag. NavigationStartingEventArgs3 supplies reload and back/forward kinds when present, and an unreadable or invalid UTF-16 URL cancels.
Block a script navigation and a link click while the page stays alive, allow a favicon.ico navigation and require its 302 hop to favicon.svg to be reported, and confirm a host navigation back is not reported.
…c gap Describe the handler contract, redirect and frame reporting, per-engine kinds, and how to leave bridge-intercepted navigations to the engine. Move native navigation policy to the closed table and add its method mapping.
Minimize, restore, maximize, kiosk, and visibility requests were issued with at most one pump between them. X11 window managers apply them asynchronously and GDK applies (un)maximize to a window it considers unmapped only locally, so a later request could race an earlier one and leave GTK's toplevel resize queue waiting forever; every later setSize was then ignored. Pump for a bounded settle period after each change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes several open semantic gaps recorded in
docs/PURE_ZIG_REFACTOR.mdagainst upstream WebUI (webui-dev/webuiatf1b28ee).Lifecycle
Running.wait()now follows upstream per-window semantics: a window that is closed on purpose ends immediately, an ordinary disconnect gets the 1.5 s reload grace period, andApp.Options.startup_timeout(default 15 s,nullwaits forever) bounds the wait for the first connection to a shown window. Request activity extends that deadline, as upstream does.Running.requestExit()closes every window and wakes the waiter. Concurrent waiters geterror.AlreadyWaiting.App.createWindow()also works while the app runs, like upstreamwebui_new_window. The new window gets fresh credentials, its folder, and its own folder monitor.App.destroyWindow()mirrorswebui_destroy. It unregisters the window at once, so its routes answer404, connected pages get a backend close, and later messages close the transport with1001. It then cancels the window's handlers, monitor, and managed browser in the background. The window is freed after its last connection, request, and deferred reply, andRunning.stop()finishes any pending cleanup. It is safe to call from the window's own handlers, through the newClient.window().concurrentbefore the server starts.Callback metadata
Call.name,Call.origin(.callor.click), andCall.cookies/Event.cookieswithcookie(name)expose the binding name, the call origin, and a per-connection copy of the upgrade'sCookieheader.Limits.max_cookie_size. Upgrades over the limit get431.Content and routing
favicon.icoandfavicon.svgare resolved from the custom icon first, then a readable directory file, then upstream's built-in SVG.favicon.icoanswers302tofavicon.svg. This applies at both the capability root and the origin root.Content.sitecombines optional embedded HTML, a resource handler, a root folder, and an entry file. Requests resolve in upstream order: handler, then virtual-directory index probing, then root HTML, then folder files, then the default favicon or404. A handler declines a path by answering an empty404.Site.entrymakes the root redirect to a validated relative entry file, likewebui_show(window, "page.html")..customcontent now gets the same virtual-index probing.Window.installContent()replaces resources without navigating clients. If either the current or the new content is an external URL, it returnserror.NavigationRequired.Firefox app mode
chrome/userChrome.cssand a rewrittenuser.js. Theuser.jsenables the stylesheet and turns off the default-browser check, the close warning, tabs in the title bar, and the first-run pages.high_contrast = falsenow works for Firefox through thebrowser.display.document_color_usepreference. A caller profile is never modified, so with one Firefox still returnserror.UnsupportedBrowserHighContrast./tmp. When/snap/bin/firefoxexists, Firefox profiles move into the snap's user directory, as upstream does. The home directory comes from the passwd entry, becausestd.Ioexposes no process environment.managedProfileDirectoryand the profile helpers now take anIo.Browser discovery
chrome.exe. Like upstream, a folder with Google'sinitial_preferencesormaster_preferencesfile is Google Chrome, and any otherchrome.exeis Chromium. Registered Chromium is now found, and a Chromiumchrome.exeis no longer reported as Chrome.~/Applications, then looks up the browser's bundle identifier through Spotlight (mdfind), which finds the bundle wherever it is installed. Upstream usesopen -R -a, which reveals the app in Finder.mdfindhas no side effects. A missing or disabled index just finds nothing.Native page titles
notify::title, WebView2 toDocumentTitleChanged, and Cocoa observesWKWebView.titlethrough KVO. KVO also catchesdocument.titlechanges after load, which upstream'sdidFinishNavigationmisses.Options.titleis the initial title, andsetTitleapplies at once until the page reports another one. An empty title keeps the host title, except that WebView2 reports its own default for untitled documents.Options.follow_page_title/Window.setFollowPageTitle()keep the title under host control, andWindow.title()reads the host title. The native smoke checks following,setTitle, empty titles, and a window with following turned off, on every platform.@alignCastfixes theaarch64-windowsnative build.Frameless drag and resize
--webui-app-regionvalue. Once the primary button moves over adragarea, it posts on a dedicated message channel and the host starts a window-manager move. The host checks that the primary button is actually held, so a page cannot move the window by posting on its own. Resizable frameless windows resize from upstream's 6 px edge band, with matching resize cursors.ICoreWebView2Settings9non-client regions, so CSSapp-region: dragareas act as the caption (upstream PR #718). Runtimes without Settings9 report that drag regions are unavailable. Resizable frameless windows keep aWS_THICKFRAMEsizing border.native.Window.dragRegion()reports the active model (.webui_property,.css_app_region,.window_background, or.none).mouse_eventon Windows). A drag must move the window, and a right-edge drag must widen it. On GTK, a forged drag message without a held button must not move the window. macOS reads back the background-movement setting, because synthesizing input there needs Accessibility permission. CI installs xdotool and passes--require-input, so missing tools fail instead of skipping. Breaking the button check, the move, or the edge resize each makes the smoke fail.zig build test, and the edge hit-test has a direct unit test.Engine-level navigation decisions
native.Options.navigation_handler/Window.setNavigationHandler()decide page and frame navigations in the engine, like upstream's WebKitGTKdecide-policyhandler, on all three backends (GTKdecide-policy, WKWebViewdecidePolicyForNavigationAction, WebView2NavigationStarting+FrameNavigationStarting). The handler gets a borrowed{ url, kind }and returnsfalseto cancel and keep the page.navigate()are not reported. Every server redirect hop is reported, because WKWebView has no public redirect flag and this is the only behavior consistent across engines. It also lets a handler stop a redirect to another origin. A URL that cannot be read cancels the navigation.favicon.iconavigation to report its302hop tofavicon.svg, and checks that a host navigation back is not reported. Mutations (no host exemption, ignored decision, and on GTK skipped redirects) each fail it on Linux and macOS.Tests and tooling
zig build test -Dtest-filter=<name>runs selected tests.CHECK_TOKENis acknowledged before the client is registered.App.destroyWindow()now sends the close notification before starting the background reaper. Before, a handler destroying its own window could be cancelled mid-broadcast on Linux.Validation
zig build test(74/74) andzig buildon macOS with Zig 0.16.0. The full suite also passed 40 runs with 8 copies in parallel.zig build -Dtarget=for x86_64-linux, aarch64-linux, x86_64-windows, x86_64-macos, and aarch64-macosrg 'webui_new|pub extern fn webui_' srcfinds nothing