From a8eb1834ebc628abe1c65a7ab78377e882b69ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Tue, 29 Sep 2026 18:58:51 -0300 Subject: [PATCH] Add tabbed HTML browsing and extend eepp navigation and input support eepp framework: - Add shared cookie jars to UISceneNode and UIWebView, plus versioned CookieManager persistence, domain clearing, and locking in hasCookie(). - Follow HTTP 303 redirects and merge same-host redirect cookies as name/value pairs without forwarding Set-Cookie attributes. - Include Cookie headers in document cache keys while preserving static resource reuse as cookies change. - Submit HTML forms with an empty action to the current document URL. - Extend NavigationRequest with source, mouse buttons, modifiers, and navigation target; anchors forward this context to the scene. - Emit OnLinkOpenRequested for middle-click, platform-modifier click, and explicit new-tab requests, with current-view fallback if unhandled. - Install the WebView navigation interceptor during construction. - Dispatch middle clicks to the hovered node without moving keyboard focus. - Add the clicked target to ContextMenuEvent so applications can extend context menus through existing events. - Parse HTML titles, emit TitleEvent, and fix onTitleChanged's payload cast. - Load declared favicons through the shared image cache, emit OnFaviconChanged, and ignore responses from superseded navigation. - Add optional UITabWidgetSplitter tab-bar visibility while split; keep it disabled by default and respect explicitly hidden tab bars. - Add Space/Shift+Space scrolling and viewport-sized Page Up/Down steps to UIScrollView, shared by WebView and Markdown content. - Implement setEnableDefaultKeybindings() and preserve text-input priority through normal bubbling without control-type checks. - Report inserted UICodeEditor text as handled. - Add UITextView::onFontColorChanged() and synchronize password-mask color. - Support validated left, middle, and right clicks in the inspector server and Python client. ui_html example: - Use UITabWidgetSplitter for tabs and user-controlled splits. - Share cookies and the resource cache across tabs; persist cookies between sessions and open new tabs blank. - Add Inspect, Clear Domain Cookies, Clear All Cookies, and contextual Open Link in New Tab actions through menu event subscriptions. - Open requested links in background tabs and synchronize navigation UI. - Use document titles and 16dp favicons for tabs, cap width at 200dp, and truncate long titles with ellipsis. - Enable tab-bar visibility across split panes. - Fixed: the selection rectangle now uses the visible password symbols instead of the original text. Update HTML compatibility and inspector documentation and add regression coverage for persistence, redirects, cache isolation, navigation, menus, titles, favicons, password colors, split visibility, and scrolling. --- docs/articles/ui_html_compatibility.md | 30 ++ docs/articles/ui_inspector.md | 18 +- include/eepp/network/cookiemanager.hpp | 8 + include/eepp/scene/event.hpp | 2 + include/eepp/ui/tools/uitabwidgetsplitter.hpp | 9 + include/eepp/ui/uipopupmenu.hpp | 8 +- include/eepp/ui/uiscenenode.hpp | 17 +- include/eepp/ui/uiscrollview.hpp | 5 + include/eepp/ui/uitextinput.hpp | 2 + include/eepp/ui/uitextview.hpp | 2 + include/eepp/ui/uiwebview.hpp | 36 +++ projects/scripts/eepp-inspect.py | 5 +- src/eepp/network/cookiemanager.cpp | 62 +++- src/eepp/network/http.cpp | 36 ++- src/eepp/scene/eventdispatcher.cpp | 21 +- src/eepp/ui/tools/uiinspectorserver.cpp | 14 +- src/eepp/ui/tools/uitabwidgetsplitter.cpp | 27 +- src/eepp/ui/uicodeeditor.cpp | 6 +- src/eepp/ui/uihtmlform.cpp | 4 +- src/eepp/ui/uiscenenode.cpp | 8 +- src/eepp/ui/uiscrollview.cpp | 37 ++- src/eepp/ui/uitextinput.cpp | 5 + src/eepp/ui/uitextselectioncontroller.cpp | 2 +- src/eepp/ui/uitextspan.cpp | 10 +- src/eepp/ui/uitextview.cpp | 5 +- src/eepp/ui/uiwebview.cpp | 103 +++++-- src/eepp/ui/webresourcecache.cpp | 8 +- src/examples/ui_html/ui_html.cpp | 229 ++++++++++++-- src/tests/unit_tests/cookiemanager_tests.cpp | 29 ++ src/tests/unit_tests/http.cpp | 45 +++ src/tests/unit_tests/uihtml_tests.cpp | 136 +++++++++ src/tests/unit_tests/uihtmlform_tests.cpp | 24 ++ src/tests/unit_tests/uiinspector_tests.cpp | 30 ++ src/tests/unit_tests/uiscenenode_tests.cpp | 24 ++ .../unit_tests/uitabwidgetsplitter_tests.cpp | 44 +++ src/tests/unit_tests/uiwebview_tests.cpp | 282 ++++++++++++++++++ .../unit_tests/webresourcecache_tests.cpp | 20 ++ 37 files changed, 1261 insertions(+), 92 deletions(-) diff --git a/docs/articles/ui_html_compatibility.md b/docs/articles/ui_html_compatibility.md index 2f6d0d27b..2e519b93a 100644 --- a/docs/articles/ui_html_compatibility.md +++ b/docs/articles/ui_html_compatibility.md @@ -1005,8 +1005,36 @@ back into the owning `UIWebView`, including relative URL resolution. This means ordinary links can navigate the current web view. +`NavigationRequest` carries the source node, mouse buttons, and keyboard modifiers. Middle click, +the platform modifier plus left click, or an explicit `Target::NewTab` request emits +`Event::OnLinkOpenRequested` on the WebView. Subscribe to this event, open the resolved URI in a +new view, and call `UIWebView::LinkOpenEvent::accept()` to handle it. Unhandled requests navigate +the current view. Applications add their own link menu actions through `OnCreateContextMenu`; +`ContextMenuEvent::getTarget()` identifies the clicked document node. + +`getTitle()` returns the parsed HTML document title. `onTitleChanged()` reports the title after +loading, including an empty title for pages without one. The `ui_html` example uses it for tab labels +and falls back to the host or URL. Tabs cap their width at 200dp and truncate long titles with ellipsis. +The example enables `UITabWidgetSplitter::setShowTabBarWhenSplit(true)` to keep each pane's tab bar +visible while split. This option is disabled by default for other splitter users. + +`` (including `rel="shortcut icon"`) loads the declared favicon through +the document's shared image cache. Relative URLs resolve against the document URL, and SVG icons +are supported. `Event::OnFaviconChanged` carries a `UIWebView::FaviconEvent` with the loaded texture; +an empty icon clears the previous document's favicon. Responses from an older navigation are ignored. +The `ui_html` example displays the texture as a 16dp tab icon. + `UIWebView` maintains its own navigation history. +`UIMarkdownView` uses the same shortcuts through its enclosing `UIScrollView`; disable them on +that scroll view with `setEnableDefaultKeybindings(false)`. + +Default scrolling shortcuts are Space and Page Down (one viewport down), Shift+Space and Page Up +(one viewport up). Keys handled by a focused descendant do not reach the WebView. While text input is active, +Space scrolls only if its committed text reaches the WebView unhandled; custom controls can consume +it through `onTextInput()` like built-in editors. Call `setEnableDefaultKeybindings(false)` to disable scrolling +shortcuts, including the inherited arrow and Home/End bindings. + Anchor interaction also cooperates with document text selection so a completed text drag does not accidentally activate the link. @@ -1074,6 +1102,8 @@ other text-like types -> UIHTMLTextInput Forms collect named control values and can submit navigation requests. +An omitted or empty form `action` submits to the full current document URL, preserving its query. + Supported submission encodings include: ```text diff --git a/docs/articles/ui_inspector.md b/docs/articles/ui_inspector.md index e605f20b1..7ba0fd2ca 100644 --- a/docs/articles/ui_inspector.md +++ b/docs/articles/ui_inspector.md @@ -54,6 +54,7 @@ python3 projects/scripts/eepp-inspect.py focus --scene scene:2 python3 projects/scripts/eepp-inspect.py screenshot --scene scene:2 --format png python3 projects/scripts/eepp-inspect.py screenshot --window win:2 --rect 10 20 400 300 --format webp python3 projects/scripts/eepp-inspect.py click --scene scene:2 '#submit' +python3 projects/scripts/eepp-inspect.py click --button right --scene scene:2 '#content' python3 projects/scripts/eepp-inspect.py key --scene scene:2 Enter python3 projects/scripts/eepp-inspect.py type --scene scene:2 'hello world' python3 projects/scripts/eepp-inspect.py type --target '#search' 'hello world' @@ -114,7 +115,7 @@ All examples below omit `id` where the surrounding explanation is enough; real r | `ui.inspect` | Exactly one of `handle` or `selector` plus optional `scene`; optional `properties`, `maxStringLength:4096` (maximum 65536) | Handle, scene, typed `properties`, `unavailable` | `invalid-widget`, `target-not-found`, `target-ambiguous` | | `ui.focus` | `scene` default | Scene and focused widget summary or `null` | `invalid-scene` | | `ui.screenshot` | Optional `scene` or `window` (mutually exclusive); optional `rect:[x,y,width,height]`; `format:"png"` | Temporary file path, format, window/scene handles, captured rectangle and size | `invalid-window`, `invalid-scene`, `invalid-params`, `screenshot-failed` | -| `input.click` | Exactly one target; optional `button:"left"`, `count:1` (maximum 2) | Target handle, scene, click point | `target-not-interactable`, `permission-denied` | +| `input.click` | Exactly one target; optional `button:"left"`, `"middle"`, or `"right"`, `count:1` (maximum 2) | Target handle, scene, click point | `target-not-interactable`, `permission-denied` | | `input.key` | `scene` default, `key` required, `action:"press"`, `modifiers:[]` | Key and action | `invalid-params`, `permission-denied` | | `input.text` | `scene` default, `text` required (maximum 64 KiB UTF-8) | Character count | `invalid-params`, `permission-denied` | | `watch.subscribe` | One handle or selector; explicit `properties` array; `initial:true` | Subscription and bound target values | `target-not-found`, `property-unknown` | @@ -159,7 +160,20 @@ These compact request/response pairs show each method's envelope. Handle numbers `ui.screenshot` captures the native window containing the selected scene. With no target it captures the default scene's full native window. An explicit `window` captures that whole window; an explicit `scene` crops to its visible world bounds, including a WebView document viewport. `rect` overrides that crop and uses top-left native-window pixel coordinates. It must fit entirely inside the window and have positive width and height. Supported formats are `png`, `jpg`, `bmp`, `tga`, `qoi`, and `webp`; eepp's case-insensitive image extension parser also accepts `jpeg` and `jfif` as aliases for `jpg`. The response uses the canonical extension. The server renders the selected window after the current UI update, saves the image in the OS temporary directory, and returns its absolute `path`; it does not send image bytes over NDJSON. The caller is responsible for deleting the file when finished. The path is useful to a local agent; remote clients must separately retrieve the file, for example over SSH. Screenshot observation is available in read-only mode and can be used inside `session.batch`. -Single-widget methods never pick the first of multiple selector matches: zero matches return `target-not-found`, and multiple matches return `target-ambiguous`. `input.click` uses the window input route and scene hit testing; clipped, covered, invisible, disabled, or off-window targets can return `target-not-interactable`. It does not scroll a target into view. `input.key` accepts names understood by eepp's `Input::getKeyFromName()` plus `Enter` as an alias for `Return`, and modifiers `Ctrl`, `Shift`, `Alt`, `Meta`; action may be `press`, `down`, or `up`. `input.text` sends Unicode text input to the current focus path and does not focus a widget first. Read-only mode omits input capabilities and returns `permission-denied` for these methods. +Single-widget methods never pick the first of multiple selector matches: zero matches return `target-not-found`, and multiple matches return `target-ambiguous`. `input.click` uses the window input route and scene hit testing; clipped, covered, invisible, disabled, or off-window targets can return `target-not-interactable`. It does not scroll a target into view. The default button is `left`; set `button` to `middle` for a middle click or `right` to open a context menu. Other button values return `invalid-params`. `input.key` accepts names understood by eepp's `Input::getKeyFromName()` plus `Enter` as an alias for `Return`, and modifiers `Ctrl`, `Shift`, `Alt`, `Meta`; action may be `press`, `down`, or `up`. `input.text` sends Unicode text input to the current focus path and does not focus a widget first. Read-only mode omits input capabilities and returns `permission-denied` for these methods. + +### Checking context menus + +Right-click a visible target, then query the popup and its items. For a WebView, the HTML target belongs to its document scene, while the popup usually belongs to the parent application scene: + +```sh +python3 projects/scripts/eepp-inspect.py contexts +python3 projects/scripts/eepp-inspect.py click --button right --scene scene:2 '#context-target' +python3 projects/scripts/eepp-inspect.py query --scene scene:1 'popupmenu' +python3 projects/scripts/eepp-inspect.py tree --scene scene:1 --depth 5 +``` + +`input.click` aims at the target's center. Choose a visible child if a large HTML element extends beyond the window; a body whose center is off-screen will return `target-not-interactable`. The command reports the chosen point in window pixels. The same button option works in a raw or batched `input.click` request: `{"method":"input.click","params":{"scene":"scene:2","selector":"#context-target","button":"right"}}`. ## Properties diff --git a/include/eepp/network/cookiemanager.hpp b/include/eepp/network/cookiemanager.hpp index 4afc7d95c..b9149eba4 100644 --- a/include/eepp/network/cookiemanager.hpp +++ b/include/eepp/network/cookiemanager.hpp @@ -25,6 +25,14 @@ class EE_API CookieManager { /** Remove all stored cookies. */ void clear(); + /** Remove cookies stored for exactly this domain. */ + void clearDomain( const std::string& domain ); + + /** Load or save this jar to a versioned file. Loading replaces the current contents. */ + bool loadFromFile( const std::string& path ); + + bool saveToFile( const std::string& path ) const; + /** @return The number of cookie entries across all domains. */ size_t size() const; diff --git a/include/eepp/scene/event.hpp b/include/eepp/scene/event.hpp index 5107673ad..2728c7dd1 100644 --- a/include/eepp/scene/event.hpp +++ b/include/eepp/scene/event.hpp @@ -137,6 +137,8 @@ class EE_API Event { OnHideFindReplace, OnTooltipCreated, MouseWheel, + OnLinkOpenRequested, + OnFaviconChanged, NoEvent = eeINDEX_NOT_FOUND }; diff --git a/include/eepp/ui/tools/uitabwidgetsplitter.hpp b/include/eepp/ui/tools/uitabwidgetsplitter.hpp index a2647f2ea..4569766a4 100644 --- a/include/eepp/ui/tools/uitabwidgetsplitter.hpp +++ b/include/eepp/ui/tools/uitabwidgetsplitter.hpp @@ -120,6 +120,12 @@ class EE_API UITabWidgetSplitter { void setHideTabBarOnSingleTab( bool hideTabBarOnSingleTab ); + /** Whether splitting keeps single-tab bars visible. Disabled by default. + * Explicitly hidden tab bars (setHideTabBar) remain hidden. */ + bool getShowTabBarWhenSplit() const; + + void setShowTabBarWhenSplit( bool showTabBarWhenSplit ); + void setHideTabBar( bool hideTabBar ); bool getVisualSplitting() const; @@ -255,6 +261,7 @@ class EE_API UITabWidgetSplitter { bool mHideTabBar{ false }; bool mHideTabBarOnSingleTab{ true }; bool mVisualSplitting{ true }; + bool mShowTabBarWhenSplit{ false }; Float mVisualSplitEdgePercent{ 0.1 }; Mutex mTabWidgetMutex; std::function mOnTabWidgetCreateCb; @@ -274,6 +281,8 @@ class EE_API UITabWidgetSplitter { void detachWidgetEvents( UIWidget* widget ); + void updateTabBarVisibility(); + void closeAllTabs( std::vector tabs, UITabWidget::FocusTabBehavior focusTabBehavior ); void updateTabWidgetVisualSplitting(); diff --git a/include/eepp/ui/uipopupmenu.hpp b/include/eepp/ui/uipopupmenu.hpp index 1aa8447c2..eddc4b3d5 100644 --- a/include/eepp/ui/uipopupmenu.hpp +++ b/include/eepp/ui/uipopupmenu.hpp @@ -48,13 +48,17 @@ class EE_API UIPopUpMenu : public UIMenu { class EE_API ContextMenuEvent : public MouseEvent { public: ContextMenuEvent( Node* node, UIPopUpMenu* menu, const Uint32& eventType, const Vector2i& pos, - const Uint32& flags ) : - MouseEvent( node, eventType, pos, flags ), menu( menu ) {} + const Uint32& flags, const Node* target = nullptr ) : + MouseEvent( node, eventType, pos, flags ), menu( menu ), target( target ) {} UIPopUpMenu* getMenu() const { return menu; } + /** The widget under the pointer, when the menu creator provides one. */ + const Node* getTarget() const { return target; } + protected: UIPopUpMenu* menu; + const Node* target; }; }} // namespace EE::UI diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index dceb477c3..152536255 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -44,10 +44,16 @@ class UIIcon; class UIRoot; struct NavigationRequest { + enum class Target : Uint8 { Current, NewTab }; + URI uri; std::string method{ "GET" }; std::string body; std::map extraHeaders; + const Node* source{ nullptr }; + Uint32 mouseButtons{ 0 }; + Uint32 modifiers{ 0 }; + Target target{ Target::Current }; }; class EE_API UISceneNode : public SceneNode { @@ -931,9 +937,14 @@ class EE_API UISceneNode : public SceneNode { /** @return The document referer */ URI getReferer() const { return mReferer; }; - const Network::CookieManager& getCookieManager() const { return mCookieManager; } + const CookieManager& getCookieManager() const { return *mCookieManager; } - Network::CookieManager& getCookieManager() { return mCookieManager; } + CookieManager& getCookieManager() { return *mCookieManager; } + + /** Share one cookie jar across document scenes. Passing nullptr creates a fresh private jar. */ + void setCookieManager( std::shared_ptr manager ) { + mCookieManager = manager ? std::move( manager ) : std::make_shared(); + } const WebResourceCachePtr& getWebResourceCache() const { return mWebResourceCache; } @@ -1057,7 +1068,7 @@ class EE_API UISceneNode : public SceneNode { URI mURI; URI mReferer; std::function mNavigationInterceptorCb; - Network::CookieManager mCookieManager; + std::shared_ptr mCookieManager{ std::make_shared() }; /** * @brief Protected constructor. diff --git a/include/eepp/ui/uiscrollview.hpp b/include/eepp/ui/uiscrollview.hpp index 9e207cd28..aaeef1a37 100644 --- a/include/eepp/ui/uiscrollview.hpp +++ b/include/eepp/ui/uiscrollview.hpp @@ -52,6 +52,7 @@ class EE_API UIScrollView : public UITouchDraggableWidget { void setAnchorScroll( bool anchor ); + /** Enable default scrolling shortcuts. Enabled by default. */ void setEnableDefaultKeybindings( bool enable ); bool areDefaultKeybindingsEnabled() const { return mDefaultKeybindings; } @@ -92,6 +93,10 @@ class EE_API UIScrollView : public UITouchDraggableWidget { virtual Uint32 onKeyDown( const KeyEvent& event ); + virtual Uint32 onTextInput( const TextInputEvent& event ); + + void scrollByViewport( Float direction ); + void onValueChangeCb( const Event* Event ); virtual void onScrollViewSizeChange( const Event* Event ); diff --git a/include/eepp/ui/uitextinput.hpp b/include/eepp/ui/uitextinput.hpp index dc23f296a..b11243725 100644 --- a/include/eepp/ui/uitextinput.hpp +++ b/include/eepp/ui/uitextinput.hpp @@ -201,6 +201,8 @@ class EE_API UITextInput : public UITextView, public TextDocument::Client { virtual void onFontStyleChanged(); + virtual void onFontColorChanged(); + void onThemeLoaded(); virtual void onCursorPosChange(); diff --git a/include/eepp/ui/uitextview.hpp b/include/eepp/ui/uitextview.hpp index 3012a95ea..62b272107 100644 --- a/include/eepp/ui/uitextview.hpp +++ b/include/eepp/ui/uitextview.hpp @@ -179,6 +179,8 @@ class EE_API UITextView : public UIWidget { virtual void onFontStyleChanged(); + virtual void onFontColorChanged(); + virtual void onAlphaChange(); virtual void onPaddingChange(); diff --git a/include/eepp/ui/uiwebview.hpp b/include/eepp/ui/uiwebview.hpp index a358087b1..7baa57503 100644 --- a/include/eepp/ui/uiwebview.hpp +++ b/include/eepp/ui/uiwebview.hpp @@ -17,6 +17,10 @@ using namespace EE::Network; +namespace EE { namespace Network { +class CookieManager; +}} // namespace EE::Network + namespace EE { namespace UI { class UIHTMLHtml; @@ -26,6 +30,31 @@ class UISceneNode; class EE_API UIWebView : public UIScrollView { public: + /** The icon is empty when the replacement document has no favicon yet. */ + struct FaviconEvent : Scene::Event { + TexturePtr icon; + + FaviconEvent( Node* node, TexturePtr icon = {} ) : + Scene::Event( node, Event::OnFaviconChanged ), icon( std::move( icon ) ) {} + }; + + struct TitleEvent : Scene::Event { + std::string title; + + TitleEvent( Node* node, std::string title ) : + Scene::Event( node, Event::OnTitleChanged ), title( std::move( title ) ) {} + }; + + struct LinkOpenEvent : Scene::Event { + URI uri; + mutable bool handled{ false }; + + LinkOpenEvent( Node* node, URI uri ) : + Scene::Event( node, Event::OnLinkOpenRequested ), uri( std::move( uri ) ) {} + + void accept() const { handled = true; } + }; + struct NavigationEvent : Scene::Event { URI uri; bool success{ false }; @@ -72,6 +101,8 @@ class EE_API UIWebView : public UIScrollView { const URI& getCurrentURI() const; + const std::string& getTitle() const { return mTitle; } + void reload(); UIWidget* getDocumentContainer() const; @@ -82,6 +113,9 @@ class EE_API UIWebView : public UIScrollView { UIWebView* setWebResourceCache( WebResourceCachePtr cache, CachePartitionId partition = 0 ); + /** Share a cookie jar with other WebViews. Set before starting navigation. */ + UIWebView* setCookieManager( std::shared_ptr manager ); + void setStyleSheetDefaultMarker( Uint32 marker ); void setUserAgent( const std::string& userAgent ); @@ -127,6 +161,7 @@ class EE_API UIWebView : public UIScrollView { bool mIsLoading{ false }; Uint64 mNavigationGeneration{ 0 }; std::string mUserAgent; + std::string mTitle; Time mDefaultTimeout{ Seconds( 30 ) }; Time mWebResourceCachePruneElapsed; Uint32 mStyleSheetDefaultMarker{ 0 }; @@ -141,6 +176,7 @@ class EE_API UIWebView : public UIScrollView { virtual void scheduledUpdate( const Time& time ); virtual Uint32 onKeyDown( const KeyEvent& event ); + virtual Uint32 onMessage( const NodeMessage* message ); virtual void onScrollViewSizeChange( const Event* event ); diff --git a/projects/scripts/eepp-inspect.py b/projects/scripts/eepp-inspect.py index 64bdf6e70..1738889aa 100755 --- a/projects/scripts/eepp-inspect.py +++ b/projects/scripts/eepp-inspect.py @@ -110,7 +110,9 @@ def command(args): params["format"] = args.format return "ui.screenshot", params if name == "click": - return "input.click", target(args.target, args.scene) + params = target(args.target, args.scene) + params["button"] = args.button + return "input.click", params if name == "key": params.update(key=args.key, action=args.action, modifiers=args.modifiers) return "input.key", params @@ -168,6 +170,7 @@ def main(): click = sub.add_parser("click") click.add_argument("target") click.add_argument("--scene") + click.add_argument("--button", choices=("left", "middle", "right"), default="left") key = sub.add_parser("key") key.add_argument("key") key.add_argument("--scene") diff --git a/src/eepp/network/cookiemanager.cpp b/src/eepp/network/cookiemanager.cpp index cb285780f..2d3cfe5b1 100644 --- a/src/eepp/network/cookiemanager.cpp +++ b/src/eepp/network/cookiemanager.cpp @@ -1,12 +1,14 @@ #include +#include #include +#include +#include namespace EE { namespace Network { CookieManager::CookieManager() {} -void CookieManager::storeCookies( const std::string& domain, - const Http::Response& response ) { +void CookieManager::storeCookies( const std::string& domain, const Http::Response& response ) { std::string setCookie = response.getField( "set-cookie" ); if ( !setCookie.empty() ) parseSetCookie( domain, setCookie ); @@ -38,6 +40,61 @@ void CookieManager::clear() { mCookies.clear(); } +void CookieManager::clearDomain( const std::string& domain ) { + Lock l( mMutex ); + mCookies.erase( domain ); +} + +bool CookieManager::loadFromFile( const std::string& path ) { + std::ifstream file( path, std::ios::binary ); + if ( !file ) + return false; + std::string version; + if ( !( file >> version ) || version != "eepp-cookies-1" ) + return false; + UnorderedMap> cookies; + std::string domain, name, value; + while ( file >> std::quoted( domain ) ) { + if ( !( file >> std::quoted( name ) >> std::quoted( value ) ) || domain.empty() || + name.empty() ) + return false; + cookies[domain][name] = value; + } + if ( !file.eof() ) + return false; + Lock l( mMutex ); + mCookies = std::move( cookies ); + return true; +} + +bool CookieManager::saveToFile( const std::string& path ) const { + const std::string temporaryPath = path + ".tmp"; + { + std::ofstream file( temporaryPath, std::ios::binary | std::ios::trunc ); + if ( !file ) + return false; + file << "eepp-cookies-1\n"; + { + Lock l( mMutex ); + for ( const auto& domain : mCookies ) { + for ( const auto& cookie : domain.second ) + file << std::quoted( domain.first ) << ' ' << std::quoted( cookie.first ) << ' ' + << std::quoted( cookie.second ) << '\n'; + } + } + file.flush(); + if ( !file ) { + FileSystem::fileRemove( temporaryPath ); + return false; + } + } + if ( !FileSystem::fileMove( temporaryPath, path ) ) { + FileSystem::fileRemove( temporaryPath ); + return false; + } + return true; +} + size_t CookieManager::size() const { Lock l( mMutex ); size_t total = 0; @@ -73,6 +130,7 @@ void CookieManager::parseSetCookie( const std::string& domain, } bool CookieManager::hasCookie( const std::string& domain ) const { + Lock l( mMutex ); return mCookies.find( domain ) != mCookies.end(); } diff --git a/src/eepp/network/http.cpp b/src/eepp/network/http.cpp index e79b51192..dd6564047 100644 --- a/src/eepp/network/http.cpp +++ b/src/eepp/network/http.cpp @@ -1061,6 +1061,7 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr // redirections, send a new request to the redirection location. if ( ( received.getStatus() == Response::MovedPermanently || received.getStatus() == Response::MovedTemporarily || + received.getStatus() == Response::SeeOther || received.getStatus() == Response::PermanentRedirect || received.getStatus() == Response::TemporaryRedirect ) && request.getFollowRedirect() ) { @@ -1094,9 +1095,38 @@ Http::Response Http::downloadRequest( const Http::Request& request, IOStream& wr newRequest.setProgressCallback( request.getProgressCallback() ); - if ( received.hasField( "set-cookie" ) ) { - newRequest.setField( - "Cookie", received.getField( "set-cookie" ) ); + if ( received.hasField( "set-cookie" ) && + ( uri.getHost().empty() || + uri.getHost() == getHostName() ) ) { + const std::string& setCookie = + received.getField( "set-cookie" ); + std::string_view cookiePair = String::trim( + std::string_view( setCookie ) + .substr( 0, setCookie.find( ';' ) ) ); + const size_t equals = cookiePair.find( '=' ); + if ( equals != std::string::npos && equals != 0 ) { + std::string cookieHeader( cookiePair ); + std::string_view cookieName = + cookiePair.substr( 0, equals ); + const std::string& previous = + request.getField( "cookie" ); + String::readBySeparator( + previous, + [&]( std::string_view part ) { + std::string_view pair = + String::trim( part ); + if ( !pair.empty() && + pair.substr( 0, + pair.find( '=' ) ) != + cookieName ) { + cookieHeader += "; "; + cookieHeader.append( pair.data(), + pair.size() ); + } + }, + ';' ); + newRequest.setField( "Cookie", cookieHeader ); + } } request.mRedirectionCount++; diff --git a/src/eepp/scene/eventdispatcher.cpp b/src/eepp/scene/eventdispatcher.cpp index bfe44057e..4c08f9426 100644 --- a/src/eepp/scene/eventdispatcher.cpp +++ b/src/eepp/scene/eventdispatcher.cpp @@ -186,7 +186,7 @@ void EventDispatcher::update( const Time& time ) { } if ( mInput->getReleaseTrigger() ) { - if ( NULL != mFocusNode ) { + if ( NULL != mFocusNode || ( mInput->getReleaseTrigger() & EE_BUTTON_MMASK ) ) { if ( !nodeWasDragging || mMousePos == mLastMousePos ) { // The focused node can change after the MouseUp ( since the node can call // "setFocus()" on other node And the MouseClick would be received by the new @@ -208,14 +208,21 @@ void EventDispatcher::update( const Time& time ) { } if ( mInput->getClickTrigger() && mDownNode == mOverNode ) { - mLastFocusNode->onMouseClick( mMousePosi, mInput->getClickTrigger() ); - sendMsg( mLastFocusNode, NodeMessage::MouseClick, mInput->getClickTrigger() ); + // Middle click should activate the hovered widget without changing keyboard + // focus. + Node* clickNode = ( mInput->getClickTrigger() & EE_BUTTON_MMASK ) + ? mOverNode + : mLastFocusNode; + if ( clickNode ) { + clickNode->onMouseClick( mMousePosi, mInput->getClickTrigger() ); + sendMsg( clickNode, NodeMessage::MouseClick, mInput->getClickTrigger() ); + } - if ( mInput->getDoubleClickTrigger() && + if ( clickNode && mInput->getDoubleClickTrigger() && mClickPos.distance( mMousePosi ) < 10 ) { - mLastFocusNode->onMouseDoubleClick( mMousePosi, - mInput->getDoubleClickTrigger() ); - sendMsg( mLastFocusNode, NodeMessage::MouseDoubleClick, + clickNode->onMouseDoubleClick( mMousePosi, + mInput->getDoubleClickTrigger() ); + sendMsg( clickNode, NodeMessage::MouseDoubleClick, mInput->getDoubleClickTrigger() ); } diff --git a/src/eepp/ui/tools/uiinspectorserver.cpp b/src/eepp/ui/tools/uiinspectorserver.cpp index 05009ea0c..caf00e3c3 100644 --- a/src/eepp/ui/tools/uiinspectorserver.cpp +++ b/src/eepp/ui/tools/uiinspectorserver.cpp @@ -352,7 +352,7 @@ struct UIInspectorServer::Impl { : "unauthenticated"; const char* message = request.is_discarded() ? "Malformed JSON" - : isConnect ? "Invalid inspector token" + : isConnect ? "Invalid inspector token" : "First request must be session.connect"; client.output += json( { { "id", id }, @@ -1068,8 +1068,14 @@ struct UIInspectorServer::Impl { const std::string windowHandle = inspector.windowHandle( window ); if ( method == "input.click" ) { const std::string targetHandle = inspector.widgetHandle( target ); - if ( params.contains( "button" ) && params["button"] != "left" ) - fail( "invalid-params", "Only left click is supported" ); + if ( params.contains( "button" ) && !params["button"].is_string() ) + fail( "invalid-params", "button must be a string" ); + const std::string button = params.value( "button", "left" ); + if ( button != "left" && button != "middle" && button != "right" ) + fail( "invalid-params", "button must be left, middle, or right" ); + const Uint8 inputButton = button == "right" ? EE_BUTTON_RIGHT + : button == "middle" ? EE_BUTTON_MIDDLE + : EE_BUTTON_LEFT; unsigned count = boundedUnsigned( params, "count", 1, 2 ); auto bounds = target->getWorldBounds(); Vector2i point( static_cast( ( bounds.Left + bounds.Right ) * 0.5f ), @@ -1102,7 +1108,7 @@ struct UIInspectorServer::Impl { InputEvent press( InputEvent::MouseButtonDown ); press.WinID = window->getWindowID(); press.button = {}; - press.button.button = EE_BUTTON_LEFT; + press.button.button = inputButton; press.button.x = point.x; press.button.y = point.y; input->pushEvent( press ); diff --git a/src/eepp/ui/tools/uitabwidgetsplitter.cpp b/src/eepp/ui/tools/uitabwidgetsplitter.cpp index 8015347a6..35f12d1a8 100644 --- a/src/eepp/ui/tools/uitabwidgetsplitter.cpp +++ b/src/eepp/ui/tools/uitabwidgetsplitter.cpp @@ -180,8 +180,11 @@ UITabWidget* UITabWidgetSplitter::createTabWidget( Node* parent ) { } ); if ( mOnTabWidgetCreateCb ) mOnTabWidgetCreateCb( tabWidget ); - Lock l( mTabWidgetMutex ); - mTabWidgets.push_back( tabWidget ); + { + Lock l( mTabWidgetMutex ); + mTabWidgets.push_back( tabWidget ); + } + updateTabBarVisibility(); return tabWidget; } @@ -691,6 +694,7 @@ void UITabWidgetSplitter::onTabClosed( const TabEvent* tabEvent ) { focusSomeWidget( nullptr ); } + updateTabBarVisibility(); eeASSERT( !mTabWidgets.empty() ); eeASSERT( !( mTabWidgets.size() == 1 && mTabWidgets[0]->getTabCount() == 0 ) ); return; @@ -771,12 +775,27 @@ bool UITabWidgetSplitter::getHideTabBarOnSingleTab() const { return mHideTabBarOnSingleTab; } +void UITabWidgetSplitter::updateTabBarVisibility() { + const bool hideSingleTabBar = mHideTabBarOnSingleTab && !( mShowTabBarWhenSplit && hasSplit() ); + for ( auto* widget : mTabWidgets ) + widget->setHideTabBarOnSingleTab( hideSingleTabBar ); +} + void UITabWidgetSplitter::setHideTabBarOnSingleTab( bool hideTabBarOnSingleTab ) { if ( hideTabBarOnSingleTab != mHideTabBarOnSingleTab ) { mHideTabBarOnSingleTab = hideTabBarOnSingleTab; + updateTabBarVisibility(); + } +} - for ( auto widget : mTabWidgets ) - widget->setHideTabBarOnSingleTab( hideTabBarOnSingleTab ); +bool UITabWidgetSplitter::getShowTabBarWhenSplit() const { + return mShowTabBarWhenSplit; +} + +void UITabWidgetSplitter::setShowTabBarWhenSplit( bool showTabBarWhenSplit ) { + if ( mShowTabBarWhenSplit != showTabBarWhenSplit ) { + mShowTabBarWhenSplit = showTabBarWhenSplit; + updateTabBarVisibility(); } } diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 21071aa0b..f8c227328 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -1195,7 +1195,7 @@ Uint32 UICodeEditor::onTextInput( const TextInputEvent& event ) { if ( plugin->onTextInput( this, event ) ) return 1; - return 0; + return 1; } void UICodeEditor::updateIMELocation() { @@ -1920,8 +1920,8 @@ Uint32 UICodeEditor::onMouseUp( const Vector2i& position, const Uint32& flags ) } Uint32 UICodeEditor::onMouseWheel( const Vector2f& offset, bool flipped ) { - const Vector2i position = getEventDispatcher() ? getEventDispatcher()->getMousePos() - : Vector2i::Zero; + const Vector2i position = + getEventDispatcher() ? getEventDispatcher()->getMousePos() : Vector2i::Zero; for ( auto& plugin : mPlugins ) if ( plugin->onMouseWheel( this, position, offset, flipped ) ) return 1; diff --git a/src/eepp/ui/uihtmlform.cpp b/src/eepp/ui/uihtmlform.cpp index b6af9bda9..5b6ca9912 100644 --- a/src/eepp/ui/uihtmlform.cpp +++ b/src/eepp/ui/uihtmlform.cpp @@ -83,7 +83,9 @@ void UIHTMLForm::submit() { UISceneNode* sceneNode = getUISceneNode(); NavigationRequest request; - request.uri = URI( mAction ); + // With no action, HTML submits to the current document URL, not its asset base URI. + request.uri = mAction.empty() ? sceneNode->getReferer() : URI( mAction ); + request.source = this; if ( mEnctype == "multipart/form-data" ) { request.method = "POST"; diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index cea5c3fa6..3d3274f65 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -919,7 +919,7 @@ void UISceneNode::requestWebResource( WebResourceRequest request, return; if ( !mReferer.empty() ) request.headers.emplace( "referer", mReferer.toString() ); - std::string cookie = mCookieManager.getCookieHeader( request.uri.getAuthority() ); + std::string cookie = getCookieManager().getCookieHeader( request.uri.getAuthority() ); if ( !cookie.empty() ) request.headers["Cookie"] = std::move( cookie ); auto resourceState = mAsyncResourceLoadState; @@ -933,7 +933,7 @@ void UISceneNode::requestWebResource( WebResourceRequest request, if ( !scene ) return; if ( !result.setCookie.empty() ) - scene->mCookieManager.storeCookiesFromHeader( authority, result.setCookie ); + scene->getCookieManager().storeCookiesFromHeader( authority, result.setCookie ); if ( callback ) callback( result ); }; @@ -947,7 +947,7 @@ TexturePtr UISceneNode::requestWebTexture( WebResourceRequest request, return {}; if ( !mReferer.empty() ) request.headers.emplace( "referer", mReferer.toString() ); - std::string cookie = mCookieManager.getCookieHeader( request.uri.getAuthority() ); + std::string cookie = getCookieManager().getCookieHeader( request.uri.getAuthority() ); if ( !cookie.empty() ) request.headers["Cookie"] = std::move( cookie ); auto resourceState = mAsyncResourceLoadState; @@ -967,7 +967,7 @@ TexturePtr UISceneNode::requestWebTexture( WebResourceRequest request, if ( !scene ) return; if ( !result.setCookie.empty() ) - scene->mCookieManager.storeCookiesFromHeader( authority, result.setCookie ); + scene->getCookieManager().storeCookiesFromHeader( authority, result.setCookie ); if ( callback ) callback( result ); }; diff --git a/src/eepp/ui/uiscrollview.cpp b/src/eepp/ui/uiscrollview.cpp index d15d34744..bdb4b8270 100644 --- a/src/eepp/ui/uiscrollview.cpp +++ b/src/eepp/ui/uiscrollview.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include #include +#include namespace EE { namespace UI { @@ -488,6 +490,10 @@ void UIScrollView::setScrollControllerPosition( const Vector2f& position ) { mVScroll->setValue( maxPosition.y > 0.f ? position.y / maxPosition.y : 0.f ); } +void UIScrollView::setEnableDefaultKeybindings( bool enable ) { + mDefaultKeybindings = enable; +} + bool UIScrollView::isAutoSetClipStep() const { return mAutoSetClipStep; } @@ -505,7 +511,16 @@ void UIScrollView::setAnchorScroll( bool anchor ) { } Uint32 UIScrollView::onKeyDown( const KeyEvent& event ) { - if ( !mDefaultKeybindings || event.getSanitizedMod() ) + const auto mod = event.getSanitizedMod(); + if ( mDefaultKeybindings && event.getKeyCode() == Window::KEY_SPACE && + ( mod & ~KEYMOD_SHIFT ) == 0 ) { + // Active text input gets priority through onTextInput's normal bubbling path. + if ( getUISceneNode()->getWindow()->isTextInputActive() ) + return UITouchDraggableWidget::onKeyDown( event ); + scrollByViewport( mod & KEYMOD_SHIFT ? -1.f : 1.f ); + return 1; + } + if ( !mDefaultKeybindings || mod ) return UITouchDraggableWidget::onKeyDown( event ); if ( event.getKeyCode() == Window::KEY_UP ) { @@ -515,10 +530,10 @@ Uint32 UIScrollView::onKeyDown( const KeyEvent& event ) { mVScroll->setValue( mVScroll->getValue() + mVScroll->getClickStep() ); return 1; } else if ( event.getKeyCode() == Window::KEY_PAGEDOWN ) { - mVScroll->setValue( mVScroll->getValue() + mVScroll->getPageStep() ); + scrollByViewport( 1.f ); return 1; } else if ( event.getKeyCode() == Window::KEY_PAGEUP ) { - mVScroll->setValue( mVScroll->getValue() - mVScroll->getPageStep() ); + scrollByViewport( -1.f ); return 1; } else if ( event.getKeyCode() == Window::KEY_HOME ) { mVScroll->setValue( mVScroll->getMinValue() ); @@ -531,4 +546,20 @@ Uint32 UIScrollView::onKeyDown( const KeyEvent& event ) { return UITouchDraggableWidget::onKeyDown( event ); } +Uint32 UIScrollView::onTextInput( const TextInputEvent& event ) { + if ( mDefaultKeybindings && event.getChar() == ' ' && event.isValid( getInput() ) ) { + scrollByViewport( getInput()->isShiftPressed() ? -1.f : 1.f ); + return 1; + } + return UITouchDraggableWidget::onTextInput( event ); +} + +void UIScrollView::scrollByViewport( Float direction ) { + const Float range = getScrollControllerMaxPosition().y; + if ( mVScroll->isEnabled() && range > 0.f ) { + mVScroll->setValue( mVScroll->getValue() + + direction * mContainer->getPixelsSize().getHeight() / range ); + } +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uitextinput.cpp b/src/eepp/ui/uitextinput.cpp index 614bb9c41..fc10fca8a 100644 --- a/src/eepp/ui/uitextinput.cpp +++ b/src/eepp/ui/uitextinput.cpp @@ -406,6 +406,11 @@ void UITextInput::onFontStyleChanged() { invalidateDraw(); } +void UITextInput::onFontColorChanged() { + if ( mPassCache ) + mPassCache->setFillColor( mTextCache.getFillColor() ); +} + Text& UITextInput::getVisibleTextCache() { if ( mMode == TextInputMode::Password && mPassCache ) return *mPassCache; diff --git a/src/eepp/ui/uitextselectioncontroller.cpp b/src/eepp/ui/uitextselectioncontroller.cpp index 67550f9d6..4915ecbc6 100644 --- a/src/eepp/ui/uitextselectioncontroller.cpp +++ b/src/eepp/ui/uitextselectioncontroller.cpp @@ -810,7 +810,7 @@ bool UITextSelectionController::showContextMenu( const Vector2i& position, Uint3 } auto* selectAllItem = menu->add( mHost->i18n( "uicodeeditor_select_all", "Select All" ) ); selectAllItem->setId( "select-all" ); - ContextMenuEvent event( mHost, menu, Event::OnCreateContextMenu, position, flags ); + ContextMenuEvent event( mHost, menu, Event::OnCreateContextMenu, position, flags, target ); mHost->sendEvent( &event ); if ( menu->getCount() == 0 ) { menu->close(); diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index 6ddb715e4..d0a81bfdb 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #define PUGIXML_HEADER_ONLY #include @@ -765,8 +766,13 @@ Uint32 UIAnchorSpan::onMessage( const NodeMessage* Msg ) { if ( controller->consumeSuppressedClick() ) return 1; } - if ( !mHref.empty() && ( Msg->getFlags() & EE_BUTTON_LMASK ) ) - getUISceneNode()->openURL( mHref ); + if ( !mHref.empty() && ( Msg->getFlags() & ( EE_BUTTON_LMASK | EE_BUTTON_MMASK ) ) ) { + NavigationRequest request{ URI( mHref ) }; + request.source = this; + request.mouseButtons = Msg->getFlags(); + request.modifiers = getInput() ? getInput()->getModState() : 0; + getUISceneNode()->navigate( request ); + } return 1; } } diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 408f67a55..d8adbb366 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -306,6 +306,7 @@ UITextView* UITextView::setFontColor( const Color& color ) { mFontStyleConfig.FontColor = color; Color newColor( color.r, color.g, color.b, color.a * mAlpha / 255.f ); mTextCache.setFillColor( newColor ); + onFontColorChanged(); invalidateDraw(); } @@ -527,6 +528,8 @@ void UITextView::onFontStyleChanged() { invalidateDraw(); } +void UITextView::onFontColorChanged() {} + void UITextView::onAlphaChange() { Color color( getFontColor() ); Color newColor( color.r, color.g, color.b, color.a * mAlpha / 255.f ); @@ -649,7 +652,7 @@ void UITextView::drawSelection( Text& textCache ) { mSelRectsCache.clear(); mLastSelCurInit = selCurInit(); mLastSelCurEnd = selCurEnd(); - mSelRectsCache = mTextCache.getSelectionRects( { selCurInit(), selCurEnd() } ); + mSelRectsCache = textCache.getSelectionRects( { selCurInit(), selCurEnd() } ); } if ( !mSelRectsCache.empty() ) { diff --git a/src/eepp/ui/uiwebview.cpp b/src/eepp/ui/uiwebview.cpp index 4e0443650..5401f8f35 100644 --- a/src/eepp/ui/uiwebview.cpp +++ b/src/eepp/ui/uiwebview.cpp @@ -10,6 +10,10 @@ #include #include #include +#include + +#define PUGIXML_HEADER_ONLY +#include namespace EE { namespace UI { @@ -168,6 +172,30 @@ UIWebView::UIWebView() : UIScrollView( "webview" ) { mDocumentLayout->setParent( this ); mDocumentScene = UISceneNode::New(); + std::weak_ptr loadState( mNavigationLoadState ); + mDocumentScene->setNavigationInterceptorCb( [loadState]( const NavigationRequest& request ) { + auto locked = loadState.lock(); + if ( !locked || !locked->alive || locked->owner == nullptr ) + return true; + UIWebView* self = locked->owner; + UISceneNode* docScene = self->getDocumentSceneNode(); + if ( !docScene ) + return true; + URI uri = docScene->solveRelativePath( request.uri ); + const bool requestNewTab = request.target == NavigationRequest::Target::NewTab || + ( request.mouseButtons & EE_BUTTON_MMASK ) || + ( ( request.mouseButtons & EE_BUTTON_LMASK ) && + ( request.modifiers & KeyMod::getDefaultModifier() ) ); + if ( requestNewTab ) { + LinkOpenEvent event( self, uri ); + self->sendEvent( &event ); + if ( event.handled ) + return true; + } + self->loadURI( uri, request.method != "GET", request.method, request.body, + request.extraHeaders ); + return true; + } ); mDocumentScene->setFollowParentSize( false ); mDocumentScene->setVisibleBoundsNode( mContainer ); mDocumentScene->setParent( mDocumentLayout ); @@ -290,6 +318,7 @@ void UIWebView::loadURI( URI uri, bool isHistoryNav, const std::string& method, const std::string& body, const Http::Request::FieldTable& headers ) { Uint64 generation = beginNavigationLoad(); mIsLoading = true; + mTitle.clear(); if ( !isHistoryNav ) pushHistory( uri ); @@ -415,32 +444,66 @@ void UIWebView::loadDocumentData( URI url, std::string data, Uint64 generation ) ui->getStyleSheet().removeAllWithoutMarker( self->mStyleSheetDefaultMarker ); ui->setURIFromURL( url ); + std::string documentXML = Tools::HTMLFormatter::HTMLtoXML( data ); + pugi::xml_document metadataDocument; + if ( metadataDocument.load_string( documentXML.c_str() ) ) { + self->mTitle = String::trim( std::string_view( + metadataDocument.child( "html" ).child( "head" ).child( "title" ).child_value() ) ); + } else { + self->mTitle.clear(); + } auto hash = String::hash( url.toString() ); - ui->loadLayoutFromString( Tools::HTMLFormatter::HTMLtoXML( data ), self->mDocContainer, - hash ); + ui->loadLayoutFromString( documentXML, self->mDocContainer, hash ); self->mTextSelectionController.onDocumentChanged(); - ui->setNavigationInterceptorCb( [loadState]( const NavigationRequest& request ) { - auto locked = loadState.lock(); - if ( !locked || !locked->alive || locked->owner == nullptr ) - return true; - UIWebView* self = locked->owner; - UISceneNode* docScene = self->getDocumentSceneNode(); - if ( !docScene ) - return true; - URI uri = docScene->solveRelativePath( request.uri ); - self->loadURI( uri, request.method != "GET", request.method, request.body, - request.extraHeaders ); - return true; - } ); - if ( !self->isNavigationLoadCurrent( generation ) ) return; self->mIsLoading = false; NavigationEvent ev( self, (Uint32)Event::OnNavigationCompleted, url, true ); self->sendEvent( &ev ); + TitleEvent titleEvent( self, self->mTitle ); + self->sendEvent( &titleEvent ); self->markDocumentExtentDirty( LayoutInvalidation::Document ); self->updateDocumentMetricsIfNeeded(); + + FaviconEvent cleared( self ); + self->sendEvent( &cleared ); + if ( !self->isNavigationLoadCurrent( generation ) ) + return; + for ( auto link : metadataDocument.child( "html" ).child( "head" ).children( "link" ) ) { + std::string_view tokens( link.attribute( "rel" ).value() ); + bool isIcon = false; + while ( !tokens.empty() ) { + const size_t start = tokens.find_first_not_of( " \t\r\n\f" ); + if ( start == std::string_view::npos ) + break; + tokens.remove_prefix( start ); + const size_t end = tokens.find_first_of( " \t\r\n\f" ); + if ( String::iequals( tokens.substr( 0, end ), "icon" ) ) { + isIcon = true; + break; + } + if ( end == std::string_view::npos ) + break; + tokens.remove_prefix( end ); + } + const char* href = link.attribute( "href" ).value(); + if ( !isIcon || !*href ) + continue; + WebResourceRequest request; + request.uri = ui->solveRelativePath( URI( href ) ); + request.kind = WebResourceKind::Image; + request.timeout = self->mDefaultTimeout; + ui->requestWebTexture( std::move( request ), + [loadState, generation]( const WebResourceResult& result ) { + auto* view = resolveNavigationLoad( loadState, generation ); + if ( !view || !result.success || !result.texture ) + return; + FaviconEvent iconEvent( view, result.texture ); + view->sendEvent( &iconEvent ); + } ); + break; + } } ); } @@ -532,6 +595,12 @@ UIWebView* UIWebView::setWebResourceCache( WebResourceCachePtr cache, CacheParti return this; } +UIWebView* UIWebView::setCookieManager( std::shared_ptr manager ) { + if ( mDocumentScene ) + mDocumentScene->setCookieManager( std::move( manager ) ); + return this; +} + bool UIWebView::updateDocumentViewportMetrics() { if ( !mDocumentScene || !mDocumentLayout || !mDocContainer || mUpdatingDocumentViewportMetrics ) return false; @@ -671,7 +740,7 @@ Uint32 UIWebView::onNavigationError( std::function cb ) { return on( Event::OnTitleChanged, - [cb]( const Event* e ) { cb( static_cast( e )->error ); } ); + [cb]( const Event* e ) { cb( static_cast( e )->title ); } ); } }} // namespace EE::UI diff --git a/src/eepp/ui/webresourcecache.cpp b/src/eepp/ui/webresourcecache.cpp index 32018ec31..8486aa8cf 100644 --- a/src/eepp/ui/webresourcecache.cpp +++ b/src/eepp/ui/webresourcecache.cpp @@ -33,10 +33,10 @@ std::string makeRequestKey( CachePartitionId partition, const WebResourceRequest canonicalURI( request.uri ).c_str() ); for ( const auto& header : request.headers ) { std::string name = String::toLower( header.first ); - // The partition identifies the cookie/authentication context. Cookie values evolve while a - // document is being used, and Referer changes with navigation history; including either one - // would duplicate an otherwise reusable resource on Back/Forward navigation. - if ( name == "cookie" || name == "referer" ) + // Documents can change with authentication, while static resources remain reusable as + // cookies evolve. Referer changes with navigation history but does not identify content. + if ( ( name == "cookie" && request.kind != WebResourceKind::Document ) || + name == "referer" ) continue; key += name; key += ':'; diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index 859e1937b..ad6b2dcea 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -1,9 +1,38 @@ #include +#include #include #include #include +using namespace EE::UI::Tools; + +struct BrowserTabs : UITabWidgetSplitter::Client { + UITabWidgetSplitter* splitter{ nullptr }; + UITextInput* urlBar{ nullptr }; + UIPushButton* backBtn{ nullptr }; + UIPushButton* fwdBtn{ nullptr }; + + UIWebView* current() const { + return splitter && splitter->getCurWidget() ? splitter->getCurWidget()->asType() + : nullptr; + } + + void updateNavigation() const { + UIWebView* view = current(); + backBtn->setEnabled( view && view->canGoBack() ); + fwdBtn->setEnabled( view && view->canGoForward() ); + } + + void onTabCreated( UITab*, UIWidget* ) override {} + + void onWidgetFocusChange( UIWidget* widget ) override { + auto* view = widget->asType(); + urlBar->setText( view->getCurrentURI().toString() ); + updateNavigation(); + } +}; + EE_MAIN_FUNC int main( int argc, char** argv ) { std::shared_ptr threadPool( ThreadPool::createShared( eemax( 4, Sys::getCPUCount() ) ) ); @@ -103,6 +132,8 @@ EE_MAIN_FUNC int main( int argc, char** argv ) { auto vbox = ui->loadLayoutFromString( R"xml(