diff --git a/.gitignore b/.gitignore index 5d115b0..153bc9f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ doc/api/ .buildlog/ .history .svn/ +.vs/ migrate_working_dir/ # IntelliJ related diff --git a/common/webview_app.cc b/common/webview_app.cc index 0261c17..6a586b3 100644 --- a/common/webview_app.cc +++ b/common/webview_app.cc @@ -3,6 +3,7 @@ // can be found in the LICENSE file. #include "webview_app.h" +#include "webview_handler.h" #include @@ -76,28 +77,28 @@ class SimpleBrowserViewDelegate : public CefBrowserViewDelegate { } // namespace -WebviewApp::WebviewApp(CefRefPtr handler) { - m_handler = handler; -} +WebviewApp::WebviewApp(std::unique_ptr> plugin_channel) + : plugin_channel_(std::move(plugin_channel)) {} void WebviewApp::OnContextInitialized() { CEF_REQUIRE_UI_THREAD(); - + plugin_channel_->InvokeMethod("onCEFInitialized", nullptr); +} + +void WebviewApp::CreateBrowser(CefRefPtr handler) { // Specify CEF browser settings here. CefBrowserSettings browser_settings; browser_settings.windowless_frame_rate = 60; - - std::string url = "https://www.flutter.dev/"; - + + std::string url = "about:blank"; + CefWindowInfo window_info; window_info.SetAsWindowless(nullptr); - - // Create the first browser window. - CefBrowserHost::CreateBrowser(window_info, m_handler, url, browser_settings, - nullptr, nullptr); + CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings, + nullptr, nullptr); } CefRefPtr WebviewApp::GetDefaultClient() { // Called when a new browser window is created via the Chrome runtime UI. - return WebviewHandler::GetInstance(); + return nullptr; } diff --git a/common/webview_app.h b/common/webview_app.h index be59e27..bf061b9 100644 --- a/common/webview_app.h +++ b/common/webview_app.h @@ -12,8 +12,8 @@ // Implement application-level callbacks for the browser process. class WebviewApp : public CefApp, public CefBrowserProcessHandler { public: - WebviewApp(CefRefPtr handler); - + WebviewApp(std::unique_ptr> plugin_channel); + // CefApp methods: CefRefPtr GetBrowserProcessHandler() override { return this; @@ -32,9 +32,11 @@ class WebviewApp : public CefApp, public CefBrowserProcessHandler { // CefBrowserProcessHandler methods: void OnContextInitialized() override; CefRefPtr GetDefaultClient() override; - + + void WebviewApp::CreateBrowser(CefRefPtr handler); + private: - CefRefPtr m_handler; + std::unique_ptr> plugin_channel_; // Include the default reference counting implementation. IMPLEMENT_REFCOUNTING(WebviewApp); }; diff --git a/common/webview_handler.cc b/common/webview_handler.cc index 1395ef4..499705f 100644 --- a/common/webview_handler.cc +++ b/common/webview_handler.cc @@ -7,6 +7,11 @@ #include #include #include +#include +#include +#include +#include +#include #include "include/base/cef_callback.h" #include "include/cef_app.h" @@ -18,7 +23,11 @@ namespace { -WebviewHandler* g_instance = nullptr; +constexpr auto kEventType = "type"; +constexpr auto kEventValue = "value"; + +constexpr auto kEventTitleChanged = "titleChanged"; +constexpr auto kEventURLChanged = "urlChanged"; // Returns a data: URI with the specified contents. std::string GetDataURI(const std::string& data, const std::string& mime_type) { @@ -27,68 +36,107 @@ std::string GetDataURI(const std::string& data, const std::string& mime_type) { .ToString(); } -} // namespace +const std::optional> GetPointFromArgs( + const flutter::EncodableValue* args) { + const flutter::EncodableList* list = + std::get_if(args); + if (!list || list->size() != 2) { + return std::nullopt; + } + const auto x = std::get_if(&(*list)[0]); + const auto y = std::get_if(&(*list)[1]); + if (!x || !y) { + return std::nullopt; + } + return std::make_pair(*x, *y); +} -WebviewHandler::WebviewHandler() { - DCHECK(!g_instance); - g_instance = this; +const std::optional> GetPointAnDPIFromArgs( + const flutter::EncodableValue* args) { + const flutter::EncodableList* list = std::get_if(args); + if (!list || list->size() != 3) { + return std::nullopt; + } + const auto x = std::get_if(&(*list)[0]); + const auto y = std::get_if(&(*list)[1]); + const auto z = std::get_if(&(*list)[2]); + if (!x || !y || !z) { + return std::nullopt; + } + return std::make_tuple(*x, *y, *z); } -WebviewHandler::~WebviewHandler() { - g_instance = nullptr; } -// static -WebviewHandler* WebviewHandler::GetInstance() { - return g_instance; +WebviewHandler::WebviewHandler(flutter::BinaryMessenger* messenger, const int browser_id) { + const auto browser_id_str = std::to_string(browser_id); + const auto method_channel_name = "webview_cef/" + browser_id_str; + browser_channel_ = std::make_unique>( + messenger, + method_channel_name, + &flutter::StandardMethodCodec::GetInstance() + ); + browser_channel_->SetMethodCallHandler([this](const auto& call, auto result) { + HandleMethodCall(call, std::move(result)); + }); + + const auto event_channel_name = "webview_cef/" + browser_id_str + "/events"; + event_channel_ = std::make_unique>( + messenger, + event_channel_name, + &flutter::StandardMethodCodec::GetInstance() + ); + + auto handler = std::make_unique>( + [this]( + const flutter::EncodableValue* arguments, + std::unique_ptr>&& + events + ) { + event_sink_ = std::move(events); + return nullptr; + }, + [this](const flutter::EncodableValue* arguments) { + event_sink_ = nullptr; + return nullptr; + } + ); + + event_channel_->SetStreamHandler(std::move(handler)); } -void WebviewHandler::OnTitleChange(CefRefPtr browser, - const CefString& title) { - //todo: title change - if(onTitleChangedCb) { - onTitleChangedCb(title); - } +WebviewHandler::~WebviewHandler() {} + +void WebviewHandler::OnTitleChange(CefRefPtr browser, const CefString& title) { + EmitEvent(kEventTitleChanged, title.ToString()); } void WebviewHandler::OnAddressChange(CefRefPtr browser, - CefRefPtr frame, - const CefString& url) { - if(onUrlChangedCb) { - onUrlChangedCb(url); + CefRefPtr frame, + const CefString& url) { + if (frame->IsMain()) { + EmitEvent(kEventURLChanged, url.ToString()); } } void WebviewHandler::OnAfterCreated(CefRefPtr browser) { CEF_REQUIRE_UI_THREAD(); - - // Add to the list of existing browsers. - browser_list_.push_back(browser); + this->browser_ = browser; + this->browser_channel_->InvokeMethod("onBrowserCreated", nullptr); } bool WebviewHandler::DoClose(CefRefPtr browser) { - CEF_REQUIRE_UI_THREAD(); - // Allow the close. For windowed browsers this will result in the OS close - // event being sent. + CEF_REQUIRE_UI_THREAD(); + + this->browser_channel_->SetMethodCallHandler(nullptr); + this->browser_channel_ = nullptr; + this->browser_ = nullptr; + if (this->onBrowserClose) this->onBrowserClose(); return false; } void WebviewHandler::OnBeforeClose(CefRefPtr browser) { - CEF_REQUIRE_UI_THREAD(); - - // Remove from the list of existing browsers. - BrowserList::iterator bit = browser_list_.begin(); - for (; bit != browser_list_.end(); ++bit) { - if ((*bit)->IsSame(browser)) { - browser_list_.erase(bit); - break; - } - } - - if (browser_list_.empty()) { - // All browser windows have closed. Quit the application message loop. - CefQuitMessageLoop(); - } + // CEF_REQUIRE_UI_THREAD(); } bool WebviewHandler::OnBeforePopup(CefRefPtr browser, @@ -139,13 +187,8 @@ void WebviewHandler::CloseAllBrowsers(bool force_close) { // force_close)); return; } - - if (browser_list_.empty()) - return; - - BrowserList::const_iterator it = browser_list_.begin(); - for (; it != browser_list_.end(); ++it) - (*it)->GetHost()->CloseBrowser(force_close); +std::cout << "CloseAllBrowsers" << std::endl; + this->browser_->GetHost()->CloseBrowser(force_close); } // static @@ -160,69 +203,55 @@ bool WebviewHandler::IsChromeRuntimeEnabled() { } void WebviewHandler::sendScrollEvent(int x, int y, int deltaX, int deltaY) { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - CefMouseEvent ev; - ev.x = x; - ev.y = y; + CefMouseEvent ev; + ev.x = x; + ev.y = y; #ifndef __APPLE__ - // The scrolling direction on Windows and Linux is different from MacOS - deltaY = -deltaY; - // Flutter scrolls too slowly, it looks more normal by 10x default speed. - (*it)->GetHost()->SendMouseWheelEvent(ev, deltaX * 10, deltaY * 10); + // The scrolling direction on Windows and Linux is different from MacOS + deltaY = -deltaY; + // Flutter scrolls too slowly, it looks more normal by 10x default speed. + this->browser_->GetHost()->SendMouseWheelEvent(ev, deltaX * 10, deltaY * 10); #else - (*it)->GetHost()->SendMouseWheelEvent(ev, deltaX, deltaY); + this->browser_->GetHost()->SendMouseWheelEvent(ev, deltaX, deltaY); #endif - - - } } void WebviewHandler::changeSize(float a_dpi, int w, int h) { - this->dpi = a_dpi; - this->width = w; - this->height = h; - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - (*it)->GetHost()->WasResized(); - } + this->dpi_ = a_dpi; + this->width_ = w; + this->height_ = h; + this->browser_->GetHost()->WasResized(); } void WebviewHandler::cursorClick(int x, int y, bool up) { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - CefMouseEvent ev; - ev.x = x; - ev.y = y; - ev.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON; - if(up && is_dragging) { - (*it)->GetHost()->DragTargetDrop(ev); - (*it)->GetHost()->DragSourceSystemDragEnded(); - is_dragging = false; - } else { - (*it)->GetHost()->SendMouseClickEvent(ev, CefBrowserHost::MouseButtonType::MBT_LEFT, up, 1); - } + CefMouseEvent ev; + ev.x = x; + ev.y = y; + ev.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON; + if(up && is_dragging_) { + this->browser_->GetHost()->DragTargetDrop(ev); + this->browser_->GetHost()->DragSourceSystemDragEnded(); + is_dragging_ = false; + } else { + this->browser_->GetHost()->SendMouseClickEvent(ev, CefBrowserHost::MouseButtonType::MBT_LEFT, up, 1); } } void WebviewHandler::cursorMove(int x , int y, bool dragging) { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - CefMouseEvent ev; - ev.x = x; - ev.y = y; - if(dragging) { - ev.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON; - } - if(is_dragging && dragging) { - (*it)->GetHost()->DragTargetDragOver(ev, DRAG_OPERATION_EVERY); - } else { - (*it)->GetHost()->SendMouseMoveEvent(ev, false); - } + CefMouseEvent ev; + ev.x = x; + ev.y = y; + if(dragging) { + ev.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON; + } + if(is_dragging_ && dragging) { + this->browser_->GetHost()->DragTargetDragOver(ev, DRAG_OPERATION_EVERY); + } else { + this->browser_->GetHost()->SendMouseMoveEvent(ev, false); } } @@ -231,87 +260,65 @@ bool WebviewHandler::StartDragging(CefRefPtr browser, DragOperationsMask allowed_ops, int x, int y){ - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - CefMouseEvent ev; - ev.x = x; - ev.y = y; - ev.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON; - (*it)->GetHost()->DragTargetDragEnter(drag_data, ev, DRAG_OPERATION_EVERY); - is_dragging = true; - } + CefMouseEvent ev; + ev.x = x; + ev.y = y; + ev.modifiers = EVENTFLAG_LEFT_MOUSE_BUTTON; + this->browser_->GetHost()->DragTargetDragEnter(drag_data, ev, DRAG_OPERATION_EVERY); + is_dragging_ = true; return true; } void WebviewHandler::sendKeyEvent(CefKeyEvent ev) { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - (*it)->GetHost()->SendKeyEvent(ev); - } + this->browser_->GetHost()->SendKeyEvent(ev); } void WebviewHandler::loadUrl(std::string url) { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - (*it)->GetMainFrame()->LoadURL(url); - } + this->browser_->GetMainFrame()->LoadURL(url); } void WebviewHandler::goForward() { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - (*it)->GetMainFrame()->GetBrowser()->GoForward(); - } + this->browser_->GetMainFrame()->GetBrowser()->GoForward(); } void WebviewHandler::goBack() { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - (*it)->GetMainFrame()->GetBrowser()->GoBack(); - } + this->browser_->GetMainFrame()->GetBrowser()->GoBack(); } void WebviewHandler::reload() { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - (*it)->GetMainFrame()->GetBrowser()->Reload(); - } + this->browser_->GetMainFrame()->GetBrowser()->Reload(); } void WebviewHandler::openDevTools() { - BrowserList::const_iterator it = browser_list_.begin(); - if (it != browser_list_.end()) { - CefWindowInfo windowInfo; + CefWindowInfo windowInfo; #ifdef _WIN32 - windowInfo.SetAsPopup(nullptr, "DevTools"); + windowInfo.SetAsPopup(nullptr, "DevTools"); #endif - (*it)->GetHost()->ShowDevTools(windowInfo, this, CefBrowserSettings(), CefPoint()); - } + this->browser_->GetHost()->ShowDevTools(windowInfo, this, CefBrowserSettings(), CefPoint()); } void WebviewHandler::GetViewRect(CefRefPtr browser, CefRect &rect) { CEF_REQUIRE_UI_THREAD(); - + rect.x = rect.y = 0; - - if (width < 1) { + if (width_ < 1) { rect.width = 1; } else { - rect.width = width; + rect.width = width_; } - - if (height < 1) { + + if (height_ < 1) { rect.height = 1; } else { - rect.height = height; + rect.height = height_; } } bool WebviewHandler::GetScreenInfo(CefRefPtr browser, CefScreenInfo& screen_info) { //todo: hi dpi support - screen_info.device_scale_factor = this->dpi; + screen_info.device_scale_factor = this->dpi_; return false; } @@ -319,3 +326,86 @@ void WebviewHandler::OnPaint(CefRefPtr browser, CefRenderHandler::Pa const CefRenderHandler::RectList &dirtyRects, const void *buffer, int w, int h) { onPaintCallback(buffer, w, h); } + +void WebviewHandler::HandleMethodCall( + const flutter::MethodCall& method_call, + std::unique_ptr> result) { + + if (!this->browser_) { + result->Error("browser not ready yet"); + return; + } + + if (method_call.method_name().compare("loadUrl") == 0) { + if (const auto url = std::get_if(method_call.arguments())) { + this->loadUrl(*url); + return result->Success(); + } + } + else if (method_call.method_name().compare("setSize") == 0) { + auto tuple = GetPointAnDPIFromArgs(method_call.arguments()); + if (tuple) { + const auto [dpi, width, height] = tuple.value(); + this->changeSize( + static_cast(dpi), + static_cast(width), + static_cast(height) + ); + } + + result->Success(); + } + else if (method_call.method_name().compare("cursorClickDown") == 0) { + const auto point = GetPointFromArgs(method_call.arguments()); + this->cursorClick(point->first, point->second, false); + result->Success(); + } + else if (method_call.method_name().compare("cursorClickUp") == 0) { + const auto point = GetPointFromArgs(method_call.arguments()); + this->cursorClick(point->first, point->second, true); + result->Success(); + } + else if (method_call.method_name().compare("cursorMove") == 0) { + const auto point = GetPointFromArgs(method_call.arguments()); + this->cursorMove(point->first, point->second, false); + result->Success(); + } + else if (method_call.method_name().compare("cursorDragging") == 0) { + const auto point = GetPointFromArgs(method_call.arguments()); + this->cursorMove(point->first, point->second, true); + result->Success(); + } + else if (method_call.method_name().compare("setScrollDelta") == 0) { + const flutter::EncodableList* list = + std::get_if(method_call.arguments()); + const auto x = *std::get_if(&(*list)[0]); + const auto y = *std::get_if(&(*list)[1]); + const auto deltaX = *std::get_if(&(*list)[2]); + const auto deltaY = *std::get_if(&(*list)[3]); + this->sendScrollEvent(x, y, deltaX, deltaY); + result->Success(); + } + else if (method_call.method_name().compare("goForward") == 0) { + this->goForward(); + result->Success(); + } + else if (method_call.method_name().compare("goBack") == 0) { + this->goBack(); + result->Success(); + } + else if (method_call.method_name().compare("reload") == 0) { + this->reload(); + result->Success(); + } + else if (method_call.method_name().compare("openDevTools") == 0) { + this->openDevTools(); + result->Success(); + } + else if (method_call.method_name().compare("dispose") == 0) { + this->browser_->GetHost()->CloseBrowser(false); + result->Success(); + } + else { + result->NotImplemented(); + } +} diff --git a/common/webview_handler.h b/common/webview_handler.h index 84b6530..3d41614 100644 --- a/common/webview_handler.h +++ b/common/webview_handler.h @@ -6,9 +6,12 @@ #define CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_ #include "include/cef_client.h" +#include +#include +#include +#include #include -#include class WebviewHandler : public CefClient, public CefDisplayHandler, @@ -17,15 +20,11 @@ public CefLoadHandler, public CefRenderHandler{ public: std::function onPaintCallback; - std::function onUrlChangedCb; - std::function onTitleChangedCb; - - explicit WebviewHandler(); + std::function onBrowserClose; + + explicit WebviewHandler(flutter::BinaryMessenger* messenger, const int browser_id); ~WebviewHandler(); - - // Provide access to the single global instance of this object. - static WebviewHandler* GetInstance(); - + // CefClient methods: virtual CefRefPtr GetDisplayHandler() override { return this; @@ -79,10 +78,10 @@ public CefRenderHandler{ // Request that all existing browser windows close. void CloseAllBrowsers(bool force_close); - + // Returns true if the Chrome runtime is enabled. static bool IsChromeRuntimeEnabled(); - + void sendScrollEvent(int x, int y, int deltaX, int deltaY); void changeSize(float a_dpi, int width, int height); void cursorClick(int x, int y, bool up); @@ -93,17 +92,33 @@ public CefRenderHandler{ void goBack(); void reload(); void openDevTools(); - + private: - uint32_t width = 1; - uint32_t height = 1; - float dpi = 1.0; - bool is_dragging = false; - - // List of existing browser windows. Only accessed on the CEF UI thread. - typedef std::list> BrowserList; - BrowserList browser_list_; - + uint32_t width_ = 1; + uint32_t height_ = 1; + float dpi_ = 1.0; + bool is_dragging_ = false; + + CefRefPtr browser_; + std::unique_ptr> browser_channel_; + std::unique_ptr> event_sink_; + std::unique_ptr> event_channel_; + + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); + + template + void EmitEvent(const std::string eventType, const T& value) { + if (event_sink_) { + const auto event = flutter::EncodableValue(flutter::EncodableMap{ + {flutter::EncodableValue(kEventType), flutter::EncodableValue(eventType)}, + {flutter::EncodableValue(kEventValue), flutter::EncodableValue(value)}, + }); + event_sink_->Success(event); + } + } + // Include the default reference counting implementation. IMPLEMENT_REFCOUNTING(WebviewHandler); }; diff --git a/example/lib/main.dart b/example/lib/main.dart index a57625a..74df36a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:webview_cef/webview_cef.dart'; void main() { @@ -8,16 +9,113 @@ void main() { } class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); + + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class TabTitle { + static int _tabID = 0; + + final int tabID; + final ValueNotifier titleNotifier; + + TabTitle(String title) + : tabID = ++_tabID, + titleNotifier = ValueNotifier(title); +} + +class _MyAppState extends State with SingleTickerProviderStateMixin { + final List tabs = [TabTitle('Untitled')]; + + @override + void initState() { + super.initState(); + } @override - State createState() => _MyAppState(); + build(BuildContext context) { + return MaterialApp( + home: DefaultTabController( + length: tabs.length, + child: Scaffold( + appBar: AppBar( + title: TabBar( + indicator: null, + tabs: tabs.map((t) { + return Tab( + child: Row( + children: [ + Expanded( + child: ValueListenableBuilder( + valueListenable: t.titleNotifier, + builder: (context, value, _) => Text(t.titleNotifier.value), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + tabs.remove(t); + }); + }, + ), + ], + ), + ); + }).toList(), + ), + actions: [ + Builder( + builder: (context) { + return IconButton( + icon: const Icon(Icons.add_to_photos_rounded), + onPressed: () { + setState(() { + tabs.add(TabTitle('Untitled')); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + DefaultTabController.of(context)!.animateTo(tabs.length - 1); + }); + }); + }, + ); + }, + ), + ], + ), + body: TabBarView( + children: tabs.map((t) { + return BrowserView( + key: ValueKey(t.tabID), + onTitleChanged: (newTitle) => t.titleNotifier.value = newTitle, + ); + }).toList(), + ), + ), + ) + ); + } +} + +class BrowserView extends StatefulWidget { + final Function(String) onTitleChanged; + const BrowserView({ + super.key, + required this.onTitleChanged, + }); + + @override + State createState() => _BrowserViewState(); } -class _MyAppState extends State { +class _BrowserViewState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + final _controller = WebViewController(); final _textController = TextEditingController(); - String title = ""; @override void initState() { @@ -25,22 +123,25 @@ class _MyAppState extends State { initPlatformState(); } + @override + dispose() { + _controller.dispose(); + super.dispose(); + } + // Platform messages are asynchronous, so we initialize in an async method. Future initPlatformState() async { - String url = "https://flutter.dev/"; - _textController.text = url; - await _controller.initialize(); - await _controller.loadUrl(url); + _textController.text = 'https://flutter.dev'; + _controller.setWebviewListener(WebviewEventsListener( - onTitleChanged: (t) { - setState(() { - title = t; - }); - }, + onTitleChanged: widget.onTitleChanged, onUrlChanged: (url) { _textController.text = url; }, )); + + await _controller.initialize(); + await _controller.loadUrl(_textController.text); // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. @@ -50,81 +151,73 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - theme: ThemeData(useMaterial3: true), - home: Scaffold( - body: Column( - children: [ - SizedBox( - height: 20, - child: Text(title), - ), - Row( - children: [ - SizedBox( - height: 48, - child: MaterialButton( - onPressed: () { - _controller.reload(); - }, - child: const Icon(Icons.refresh), - ), + super.build(context); + return Column( + children: [ + Row( + children: [ + SizedBox( + height: 48, + child: MaterialButton( + onPressed: () { + _controller.reload(); + }, + child: const Icon(Icons.refresh), ), - SizedBox( - height: 48, - child: MaterialButton( - onPressed: () { - _controller.goBack(); - }, - child: const Icon(Icons.arrow_left), - ), + ), + SizedBox( + height: 48, + child: MaterialButton( + onPressed: () { + _controller.goBack(); + }, + child: const Icon(Icons.arrow_left), ), - SizedBox( - height: 48, - child: MaterialButton( - onPressed: () { - _controller.goForward(); - }, - child: const Icon(Icons.arrow_right), - ), + ), + SizedBox( + height: 48, + child: MaterialButton( + onPressed: () { + _controller.goForward(); + }, + child: const Icon(Icons.arrow_right), ), - SizedBox( - height: 48, - child: MaterialButton( - onPressed: () { - _controller.openDevTools(); - }, - child: const Icon(Icons.developer_mode), - ), + ), + SizedBox( + height: 48, + child: MaterialButton( + onPressed: () { + _controller.openDevTools(); + }, + child: const Icon(Icons.developer_mode), ), - Expanded( - child: TextField( - controller: _textController, - onSubmitted: (url) { - if (url.startsWith('http://')) { - _textController.text = url; - _controller.loadUrl(url); - } else if (url.startsWith('https://')) { - _textController.text = url; - _controller.loadUrl(url); - } else if (url.startsWith('www.')) { - _textController.text = 'https://$url'; - _controller.loadUrl('https://$url'); - } else { - _textController.text = url; - _controller.loadUrl('https://google.com/search?q=$url'); - } - }, - ), + ), + Expanded( + child: TextField( + controller: _textController, + onSubmitted: (url) { + if (url.startsWith('http://')) { + _textController.text = url; + _controller.loadUrl(url); + } else if (url.startsWith('https://')) { + _textController.text = url; + _controller.loadUrl(url); + } else if (url.startsWith('www.')) { + _textController.text = 'https://$url'; + _controller.loadUrl('https://$url'); + } else { + _textController.text = url; + _controller.loadUrl('https://google.com/search?q=$url'); + } + }, ), - ], - ), - _controller.value - ? Expanded(child: WebView(_controller)) - : const Text("not init"), - ], - )), + ), + ], + ), + _controller.value + ? Expanded(child: WebView(_controller)) + : const Text("not init"), + ], ); } } diff --git a/lib/src/webview.dart b/lib/src/webview.dart index df489a7..d62049b 100644 --- a/lib/src/webview.dart +++ b/lib/src/webview.dart @@ -8,12 +8,36 @@ import 'package:flutter/widgets.dart'; import 'webview_events_listener.dart'; const MethodChannel _pluginChannel = MethodChannel("webview_cef"); +bool _hasCallStartCEF = false; +final _cefStarted = Completer(); + +_startCEF() async { + if (!_hasCallStartCEF) { + _hasCallStartCEF = true; + _pluginChannel.invokeMethod("startCEF"); + _pluginChannel.setMethodCallHandler((call) async { + if (call.method == 'onCEFInitialized') { + _cefStarted.complete(); + } + }); + } + + await _cefStarted.future; +} + +const _kEventTitleChanged = "titleChanged"; +const _kEventURLChanged = "urlChanged"; class WebViewController extends ValueNotifier { - late Completer _creatingCompleter; + static int _id = 0; + + final Completer _creatingCompleter = Completer(); int _textureId = 0; bool _isDisposed = false; WebviewEventsListener? _listener; + late final MethodChannel _broswerChannel; + late final EventChannel _eventChannel; + StreamSubscription? _eventStreamSubscription; Future get ready => _creatingCompleter.future; @@ -24,12 +48,16 @@ class WebViewController extends ValueNotifier { if (_isDisposed) { return Future.value(); } - _creatingCompleter = Completer(); + + await _startCEF(); + try { - _textureId = await _pluginChannel.invokeMethod('init') ?? 0; - _pluginChannel.setMethodCallHandler(_methodCallhandler); - value = true; - _creatingCompleter.complete(); + final browserID = ++_id; + _broswerChannel = MethodChannel('webview_cef/$browserID'); + _broswerChannel.setMethodCallHandler(_methodCallhandler); + _textureId = await _pluginChannel.invokeMethod('createBrowser', browserID) ?? 0; + _eventChannel = EventChannel('webview_cef/$browserID/events'); + _eventStreamSubscription = _eventChannel.receiveBroadcastStream().listen(_handleBrowserEvents); } on PlatformException catch (e) { _creatingCompleter.completeError(e); } @@ -37,14 +65,25 @@ class WebViewController extends ValueNotifier { return _creatingCompleter.future; } - Future _methodCallhandler(MethodCall call) async { - if (_listener == null) return; + Future _methodCallhandler(MethodCall call) async { switch (call.method) { - case "urlChanged": - _listener?.onUrlChanged?.call(call.arguments); + case 'onBrowserCreated': + _creatingCompleter.complete(); + value = true; + return null; + } + + return null; + } + + _handleBrowserEvents(dynamic event) { + final m = event as Map; + switch (m['type']) { + case _kEventURLChanged: + _listener?.onUrlChanged?.call(m['value'] as String); return; - case "titleChanged": - _listener?.onTitleChanged?.call(call.arguments); + case _kEventTitleChanged: + _listener?.onTitleChanged?.call(m['value'] as String); return; default: } @@ -59,7 +98,8 @@ class WebViewController extends ValueNotifier { await _creatingCompleter.future; if (!_isDisposed) { _isDisposed = true; - await _pluginChannel.invokeMethod('dispose', _textureId); + await _broswerChannel.invokeMethod('dispose'); + _eventStreamSubscription?.cancel(); } super.dispose(); } @@ -70,7 +110,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod('loadUrl', url); + return _broswerChannel.invokeMethod('loadUrl', url); } /// Reloads the current document. @@ -79,7 +119,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod('reload'); + return _broswerChannel.invokeMethod('reload'); } Future goForward() async { @@ -87,7 +127,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod('goForward'); + return _broswerChannel.invokeMethod('goForward'); } Future goBack() async { @@ -95,7 +135,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod('goBack'); + return _broswerChannel.invokeMethod('goBack'); } Future openDevTools() async { @@ -103,7 +143,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod('openDevTools'); + return _broswerChannel.invokeMethod('openDevTools'); } /// Moves the virtual cursor to [position]. @@ -112,7 +152,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel + return _broswerChannel .invokeMethod('cursorMove', [position.dx.round(), position.dy.round()]); } @@ -121,7 +161,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod( + return _broswerChannel.invokeMethod( 'cursorDragging', [position.dx.round(), position.dy.round()]); } @@ -130,7 +170,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod( + return _broswerChannel.invokeMethod( 'cursorClickDown', [position.dx.round(), position.dy.round()]); } @@ -139,7 +179,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod( + return _broswerChannel.invokeMethod( 'cursorClickUp', [position.dx.round(), position.dy.round()]); } @@ -149,7 +189,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel.invokeMethod( + return _broswerChannel.invokeMethod( 'setScrollDelta', [position.dx.round(), position.dy.round(), dx, dy]); } @@ -159,7 +199,7 @@ class WebViewController extends ValueNotifier { return; } assert(value); - return _pluginChannel + return _broswerChannel .invokeMethod('setSize', [dpi, size.width, size.height]); } } diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 385e325..1e553f4 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -23,6 +23,8 @@ set(PLUGIN_NAME "webview_cef_plugin") list(APPEND PLUGIN_SOURCES "webview_cef_plugin.cpp" "webview_cef_plugin.h" + "texture_handler.cc" + "texture_handler.h" "${CMAKE_CURRENT_LIST_DIR}/../common/webview_app.cc" "${CMAKE_CURRENT_LIST_DIR}/../common/webview_app.h" "${CMAKE_CURRENT_LIST_DIR}/../common/webview_handler.cc" diff --git a/windows/texture_handler.cc b/windows/texture_handler.cc new file mode 100644 index 0000000..c3d00e2 --- /dev/null +++ b/windows/texture_handler.cc @@ -0,0 +1,66 @@ +#include "texture_handler.h" + +TextureHandler::TextureHandler(flutter::TextureRegistrar* texture_registrar) { + texture_registrar_ = texture_registrar; + m_texture_ = std::make_unique( + flutter::PixelBufferTexture([this](size_t width, size_t height) -> const FlutterDesktopPixelBuffer* { + auto buffer = pixel_buffer.get(); + // Only lock the mutex if the buffer is not null + // (to ensure the release callback gets called) + if (buffer) { + // Gets unlocked in the FlutterDesktopPixelBuffer's release callback. + buffer_mutex_.lock(); + } + return buffer; + }) + ); + texture_id_ = texture_registrar->RegisterTexture(m_texture_.get()); +} + +TextureHandler::~TextureHandler() { + texture_registrar_->UnregisterTexture(texture_id_); + m_texture_ = nullptr; + pixel_buffer = nullptr; + backing_pixel_buffer = nullptr; +} + +void TextureHandler::onPaintCallback(const void* buffer, int32_t width, int32_t height) { + const std::lock_guard lock(buffer_mutex_); + if (!pixel_buffer.get() || pixel_buffer.get()->width != width || pixel_buffer.get()->height != height) { + if (!pixel_buffer.get()) { + pixel_buffer = std::make_unique(); + pixel_buffer->release_context = &buffer_mutex_; + // Gets invoked after the FlutterDesktopPixelBuffer's + // backing buffer has been uploaded. + pixel_buffer->release_callback = [](void* opaque) { + auto mutex = reinterpret_cast(opaque); + // Gets locked just before |CopyPixelBuffer| returns. + mutex->unlock(); + }; + } + pixel_buffer->width = width; + pixel_buffer->height = height; + const auto size = width * height * 4; + backing_pixel_buffer.reset(new uint8_t[size]); + pixel_buffer->buffer = backing_pixel_buffer.get(); + } + + TextureHandler::SwapBufferFromBgraToRgba((void*)pixel_buffer->buffer, buffer, width, height); + texture_registrar_->MarkTextureFrameAvailable(texture_id_); +}; + +void TextureHandler::SwapBufferFromBgraToRgba(void* _dest, const void* _src, int width, int height) { + int32_t* dest = (int32_t*)_dest; + int32_t* src = (int32_t*)_src; + int32_t rgba; + int32_t bgra; + int length = width * height; + for (int i = 0; i < length; i++) { + bgra = src[i]; + // BGRA in hex = 0xAARRGGBB. + rgba = (bgra & 0x00ff0000) >> 16 // Red >> Blue. + | (bgra & 0xff00ff00) // Green Alpha. + | (bgra & 0x000000ff) << 16; // Blue >> Red. + dest[i] = rgba; + } +} \ No newline at end of file diff --git a/windows/texture_handler.h b/windows/texture_handler.h new file mode 100644 index 0000000..b706e79 --- /dev/null +++ b/windows/texture_handler.h @@ -0,0 +1,29 @@ +#ifndef WEBVIEW_CEF_WINDOWS_TEXTURE_HANDLER +#define WEBVIEW_CEF_WINDOWS_TEXTURE_HANDLER + +#include + +#include + +class TextureHandler { + +private: + flutter::TextureRegistrar* texture_registrar_; + int64_t texture_id_ = -1; + std::shared_ptr pixel_buffer; + std::unique_ptr backing_pixel_buffer; + std::mutex buffer_mutex_; + std::unique_ptr m_texture_; + + static void SwapBufferFromBgraToRgba(void* _dest, const void* _src, int width, int height); + +public: + TextureHandler(flutter::TextureRegistrar* texture_registrar); + ~TextureHandler(); + + int64_t texture_id() const { return texture_id_; } + + void onPaintCallback(const void* buffer, int32_t width, int32_t height); +}; + +#endif diff --git a/windows/webview_cef_plugin.cpp b/windows/webview_cef_plugin.cpp index a8faf99..5023f02 100644 --- a/windows/webview_cef_plugin.cpp +++ b/windows/webview_cef_plugin.cpp @@ -1,10 +1,5 @@ #include "webview_cef_plugin.h" -// This must be included before many other Windows headers. -#include - -// For getPlatformVersion; remove unless needed for your plugin implementation. -#include #include #include @@ -12,92 +7,24 @@ #include #include -#include -#include #include "webview_app.h" +#include "texture_handler.h" namespace webview_cef { bool init = false; - int64_t texture_id; flutter::TextureRegistrar* texture_registrar; - std::shared_ptr pixel_buffer; - std::unique_ptr backing_pixel_buffer; - std::mutex buffer_mutex_; - std::unique_ptr m_texture = std::make_unique(flutter::PixelBufferTexture([](size_t width, size_t height) -> const FlutterDesktopPixelBuffer* { - auto buffer = pixel_buffer.get(); - // Only lock the mutex if the buffer is not null - // (to ensure the release callback gets called) - if (buffer) { - // Gets unlocked in the FlutterDesktopPixelBuffer's release callback. - buffer_mutex_.lock(); - } - return buffer; - })); - CefRefPtr handler(new WebviewHandler()); - CefRefPtr app(new WebviewApp(handler)); + flutter::BinaryMessenger* messenger; + + CefRefPtr app; CefMainArgs mainArgs; - std::unique_ptr< - flutter::MethodChannel, - std::default_delete>> - channel = nullptr; - - - void SwapBufferFromBgraToRgba(void* _dest, const void* _src, int width, int height) { - int32_t* dest = (int32_t*)_dest; - int32_t* src = (int32_t*)_src; - int32_t rgba; - int32_t bgra; - int length = width * height; - for (int i = 0; i < length; i++) { - bgra = src[i]; - // BGRA in hex = 0xAARRGGBB. - rgba = (bgra & 0x00ff0000) >> 16 // Red >> Blue. - | (bgra & 0xff00ff00) // Green Alpha. - | (bgra & 0x000000ff) << 16; // Blue >> Red. - dest[i] = rgba; - } - } void startCEF() { CefWindowInfo window_info; CefBrowserSettings settings; window_info.SetAsWindowless(nullptr); - handler.get()->onPaintCallback = [](const void* buffer, int32_t width, int32_t height) { - const std::lock_guard lock(buffer_mutex_); - if (!pixel_buffer.get() || pixel_buffer.get()->width != width || pixel_buffer.get()->height != height) { - if (!pixel_buffer.get()) { - pixel_buffer = std::make_unique(); - pixel_buffer->release_context = &buffer_mutex_; - // Gets invoked after the FlutterDesktopPixelBuffer's - // backing buffer has been uploaded. - pixel_buffer->release_callback = [](void* opaque) { - auto mutex = reinterpret_cast(opaque); - // Gets locked just before |CopyPixelBuffer| returns. - mutex->unlock(); - }; - } - pixel_buffer->width = width; - pixel_buffer->height = height; - const auto size = width * height * 4; - backing_pixel_buffer.reset(new uint8_t[size]); - pixel_buffer->buffer = backing_pixel_buffer.get(); - } - - SwapBufferFromBgraToRgba((void*)pixel_buffer->buffer, buffer, width, height); - texture_registrar->MarkTextureFrameAvailable(texture_id); - }; - - handler.get()->onUrlChangedCb = [](std::string url) { - channel->InvokeMethod("urlChanged", std::make_unique(url)); - }; - - handler.get()->onTitleChangedCb = [](std::string title) { - channel->InvokeMethod("titleChanged", std::make_unique(title)); - }; - CefSettings cefs; cefs.windowless_rendering_enabled = true; CefInitialize(mainArgs, cefs, app.get(), nullptr); @@ -118,43 +45,27 @@ namespace webview_cef { return std::nullopt; } - static const std::optional> GetPointFromArgs( - const flutter::EncodableValue* args) { - const flutter::EncodableList* list = - std::get_if(args); - if (!list || list->size() != 2) { - return std::nullopt; - } - const auto x = std::get_if(&(*list)[0]); - const auto y = std::get_if(&(*list)[1]); - if (!x || !y) { - return std::nullopt; - } - return std::make_pair(*x, *y); - } - // static void WebviewCefPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { texture_registrar = registrar->texture_registrar(); - channel = + messenger = registrar->messenger(); + auto plugin_channel = std::make_unique>( - registrar->messenger(), "webview_cef", - &flutter::StandardMethodCodec::GetInstance()); + messenger, "webview_cef", &flutter::StandardMethodCodec::GetInstance()); auto plugin = std::make_unique(); - - channel->SetMethodCallHandler( + plugin_channel->SetMethodCallHandler( [plugin_pointer = plugin.get()](const auto& call, auto result) { plugin_pointer->HandleMethodCall(call, std::move(result)); }); - + app = new WebviewApp(std::move(plugin_channel)); registrar->AddPlugin(std::move(plugin)); } void WebviewCefPlugin::sendKeyEvent(CefKeyEvent ev) { - handler.get()->sendKeyEvent(ev); + // handler.get()->sendKeyEvent(ev); } WebviewCefPlugin::WebviewCefPlugin() {} @@ -164,74 +75,25 @@ namespace webview_cef { void WebviewCefPlugin::HandleMethodCall( const flutter::MethodCall& method_call, std::unique_ptr> result) { - if (method_call.method_name().compare("init") == 0) { + if (method_call.method_name().compare("startCEF") == 0) { if (!init) { - texture_id = texture_registrar->RegisterTexture(m_texture.get()); new std::thread(startCEF); init = true; } - result->Success(flutter::EncodableValue(texture_id)); - } - else if (method_call.method_name().compare("loadUrl") == 0) { - if (const auto url = std::get_if(method_call.arguments())) { - handler.get()->loadUrl(*url); - return result->Success(); - } - } - else if (method_call.method_name().compare("setSize") == 0) { - const flutter::EncodableList* list = - std::get_if(method_call.arguments()); - const auto dpi = *std::get_if(&(*list)[0]); - const auto width = *std::get_if(&(*list)[1]); - const auto height = *std::get_if(&(*list)[2]); - handler.get()->changeSize((float)dpi, (int)std::round(width), (int)std::round(height)); - result->Success(); - } - else if (method_call.method_name().compare("cursorClickDown") == 0) { - const auto point = GetPointFromArgs(method_call.arguments()); - handler.get()->cursorClick(point->first, point->second, false); - result->Success(); - } - else if (method_call.method_name().compare("cursorClickUp") == 0) { - const auto point = GetPointFromArgs(method_call.arguments()); - handler.get()->cursorClick(point->first, point->second, true); - result->Success(); - } - else if (method_call.method_name().compare("cursorMove") == 0) { - const auto point = GetPointFromArgs(method_call.arguments()); - handler.get()->cursorMove(point->first, point->second, false); - result->Success(); - } - else if (method_call.method_name().compare("cursorDragging") == 0) { - const auto point = GetPointFromArgs(method_call.arguments()); - handler.get()->cursorMove(point->first, point->second, true); - result->Success(); - } - else if (method_call.method_name().compare("setScrollDelta") == 0) { - const flutter::EncodableList* list = - std::get_if(method_call.arguments()); - const auto x = *std::get_if(&(*list)[0]); - const auto y = *std::get_if(&(*list)[1]); - const auto deltaX = *std::get_if(&(*list)[2]); - const auto deltaY = *std::get_if(&(*list)[3]); - handler.get()->sendScrollEvent(x, y, deltaX, deltaY); - result->Success(); - } - else if (method_call.method_name().compare("goForward") == 0) { - handler.get()->goForward(); - result->Success(); - } - else if (method_call.method_name().compare("goBack") == 0) { - handler.get()->goBack(); - result->Success(); - } - else if (method_call.method_name().compare("reload") == 0) { - handler.get()->reload(); - result->Success(); - } - else if (method_call.method_name().compare("openDevTools") == 0) { - handler.get()->openDevTools(); result->Success(); + } else if (method_call.method_name().compare("createBrowser") == 0) { + auto const browser_id = *std::get_if(method_call.arguments()); + auto handler = new WebviewHandler(messenger, browser_id); + auto texture_handler = new TextureHandler(texture_registrar); + handler->onPaintCallback = [texture_handler](const void* buffer, int32_t width, int32_t height) { + texture_handler->onPaintCallback(buffer, width, height); + }; + handler->onBrowserClose = [texture_handler] () mutable { + delete texture_handler; + }; + + app->CreateBrowser(handler); + result->Success(flutter::EncodableValue(texture_handler->texture_id())); } else { result->NotImplemented(); diff --git a/windows/webview_cef_plugin.h b/windows/webview_cef_plugin.h index dff658b..64a2534 100644 --- a/windows/webview_cef_plugin.h +++ b/windows/webview_cef_plugin.h @@ -11,23 +11,23 @@ namespace webview_cef { class WebviewCefPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); - static void sendKeyEvent(CefKeyEvent ev); +public: + static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); + static void sendKeyEvent(CefKeyEvent ev); - WebviewCefPlugin(); + WebviewCefPlugin(); - virtual ~WebviewCefPlugin(); + virtual ~WebviewCefPlugin(); - // Disallow copy and assign. - WebviewCefPlugin(const WebviewCefPlugin&) = delete; - WebviewCefPlugin& operator=(const WebviewCefPlugin&) = delete; + // Disallow copy and assign. + WebviewCefPlugin(const WebviewCefPlugin&) = delete; + WebviewCefPlugin& operator=(const WebviewCefPlugin&) = delete; private: - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result); + // Called when a method is called on this plugin's channel from Dart. + void HandleMethodCall( + const flutter::MethodCall &method_call, + std::unique_ptr> result); }; } // namespace webview_cef