├── .github └── workflows │ └── cmake.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── cmake ├── EnableLLD.cmake ├── EnableLagom.cmake ├── InstallRules.cmake └── LadybirdInstallConfig.cmake ├── demo ├── CMakeLists.txt ├── Info.plist └── main.c ├── examples ├── README.md └── example.py ├── screenshot.png └── src ├── .gitignore ├── AudioCodecPluginLadybird.cpp ├── AudioCodecPluginLadybird.h ├── BrowserWindow.cpp ├── BrowserWindow.h ├── CMakeLists.txt ├── ConsoleWidget.cpp ├── ConsoleWidget.h ├── ContentViewImpl.cpp ├── ContentViewImpl.h ├── Embed ├── webcontentview.cpp ├── webcontentview.h ├── webembed.cpp └── webembed.h ├── EventLoopImplementationGLib.cpp ├── EventLoopImplementationGLib.h ├── EventLoopImplementationGtk.cpp ├── EventLoopImplementationGtk.h ├── FontPluginPango.cpp ├── FontPluginPango.h ├── HelperProcess.cpp ├── HelperProcess.h ├── Icons ├── back.svg ├── forward.svg ├── ladybird.png └── reload.svg ├── ImageCodecPluginLadybird.cpp ├── ImageCodecPluginLadybird.h ├── InspectorWidget.cpp ├── InspectorWidget.h ├── LocationEdit.cpp ├── LocationEdit.h ├── ModelTranslator.cpp ├── ModelTranslator.h ├── README.md ├── RequestManagerSoup.cpp ├── RequestManagerSoup.h ├── SQLServer ├── CMakeLists.txt └── main.cpp ├── Settings.cpp ├── Settings.h ├── SettingsDialog.cpp ├── SettingsDialog.h ├── Tab.cpp ├── Tab.h ├── Utilities.cpp ├── Utilities.h ├── WebContent ├── CMakeLists.txt ├── MacOSSetup.h ├── MacOSSetup.mm └── main.cpp ├── WebContentView.cpp ├── WebContentView.h ├── WebDriver ├── CMakeLists.txt └── main.cpp ├── WebSocketClientManagerLadybird.cpp ├── WebSocketClientManagerLadybird.h ├── WebSocketImplQt.cpp ├── WebSocketImplQt.h ├── WebSocketLadybird.cpp ├── WebSocketLadybird.h └── config.toml /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 11 | BUILD_TYPE: Release 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: "pages" 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | build: 27 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 28 | # You can convert this to a matrix build if you need cross-platform coverage. 29 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 30 | runs-on: ubuntu-latest 31 | container: 32 | image: fedora:38 33 | steps: 34 | - name: Install dependencies 35 | run: sudo dnf -y install git cmake libglvnd-devel ninja-build ccache gcc gcc-c++ gi-docgen glibmm2.68-devel gtkmm4.0-devel libsoup3-devel 36 | 37 | - uses: actions/checkout@v3 38 | with: 39 | submodules: 'recursive' 40 | 41 | - name: Configure CMake 42 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 43 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 44 | run: cmake -B ${{github.workspace}}/build -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DDOCUMENTATION=ON -DINTROSPECTION=ON 45 | 46 | - name: Build 47 | # Build your program with the given configuration 48 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 49 | 50 | - name: Test 51 | working-directory: ${{github.workspace}}/build 52 | # Execute tests defined by the CMake configuration. 53 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 54 | run: ctest -C ${{env.BUILD_TYPE}} 55 | 56 | # Single deploy job since we're just deploying 57 | deploy: 58 | needs: build 59 | environment: 60 | name: github-pages 61 | url: ${{ steps.deployment.outputs.page_url }} 62 | runs-on: ubuntu-latest 63 | steps: 64 | - name: Setup Pages 65 | uses: actions/configure-pages@v3 66 | - name: Upload artifact 67 | uses: actions/upload-pages-artifact@v1 68 | with: 69 | path: '${{github.workspace}}/build/docs' 70 | - name: Deploy to GitHub Pages 71 | id: deployment 72 | uses: actions/deploy-pages@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | *.config 4 | *.creator 5 | *.creator.user 6 | *.creator.user.* 7 | *.files 8 | *.includes 9 | *.cflags 10 | *.cxxflags 11 | *.autosave 12 | Meta/Lagom/build 13 | Build 14 | Toolchain/Tarballs 15 | Toolchain/Build 16 | Toolchain/Local 17 | .vscode 18 | .ccls-cache 19 | .DS_Store 20 | compile_commands.json 21 | .cache 22 | .clang_complete 23 | .clangd 24 | .idea/ 25 | cmake-build-debug/ 26 | output/ 27 | run-local.sh 28 | sync-local.sh 29 | .vim/ 30 | .exrc 31 | .helix/ 32 | 33 | Userland/Libraries/LibWasm/Tests/Fixtures/SpecTests 34 | Userland/Libraries/LibWasm/Tests/Spec 35 | 36 | Tests/LibWeb/WPT/wpt 37 | Tests/LibWeb/WPT/metadata 38 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "serenity"] 2 | path = serenity 3 | url = https://github.com/SerenityOS/serenity 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16...3.22) 2 | 3 | project(ladybird 4 | VERSION 0.0.1 5 | LANGUAGES C CXX 6 | DESCRIPTION "An embedding API for LibWeb and LibJS" 7 | ) 8 | 9 | include(GNUInstallDirs) 10 | 11 | set(CMAKE_CXX_STANDARD 20) 12 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 13 | set(CMAKE_CXX_EXTENSIONS OFF) 14 | 15 | set(CMAKE_SKIP_BUILD_RPATH FALSE) 16 | set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) 17 | # See slide 100 of the following ppt :^) 18 | # https://crascit.com/wp-content/uploads/2019/09/Deep-CMake-For-Library-Authors-Craig-Scott-CppCon-2019.pdf 19 | if (NOT APPLE) 20 | set(CMAKE_INSTALL_RPATH $ORIGIN;$ORIGIN/../${CMAKE_INSTALL_LIBDIR}) 21 | endif() 22 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) 23 | 24 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 25 | 26 | include(cmake/EnableLLD.cmake) 27 | 28 | if (ENABLE_ADDRESS_SANITIZER) 29 | add_compile_options(-fsanitize=address -fno-omit-frame-pointer) 30 | add_link_options(-fsanitize=address) 31 | endif() 32 | 33 | if (ENABLE_MEMORY_SANITIZER) 34 | add_compile_options(-fsanitize=memory -fsanitize-memory-track-origins -fno-omit-frame-pointer) 35 | add_link_options(-fsanitize=memory -fsanitize-memory-track-origins) 36 | endif() 37 | 38 | if (ENABLE_UNDEFINED_SANITIZER) 39 | add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer) 40 | add_link_options(-fsanitize=undefined) 41 | endif() 42 | 43 | # Lagom 44 | # FIXME: PROJECT_IS_TOP_LEVEL with CMake 3.21+ 45 | set(LADYBIRD_IS_TOP_LEVEL FALSE) 46 | set(LADYBIRD_CUSTOM_TARGET_SUFFIX "-ladybird") 47 | if ("${CMAKE_BINARY_DIR}" STREQUAL "${PROJECT_BINARY_DIR}") 48 | set(LADYBIRD_IS_TOP_LEVEL TRUE) 49 | set(LADYBIRD_CUSTOM_TARGET_SUFFIX "") 50 | endif() 51 | 52 | if (LADYBIRD_IS_TOP_LEVEL) 53 | get_filename_component( 54 | SERENITY_SOURCE_DIR "${ladybird_SOURCE_DIR}/serenity" 55 | ABSOLUTE 56 | ) 57 | list(APPEND CMAKE_MODULE_PATH "${SERENITY_SOURCE_DIR}/Meta/CMake") 58 | include(cmake/EnableLagom.cmake) 59 | include(lagom_compile_options NO_POLICY_SCOPE) 60 | else() 61 | # FIXME: Use SERENITY_SOURCE_DIR in Lagom/CMakeLists.txt 62 | set(SERENITY_SOURCE_DIR "${SERENITY_PROJECT_ROOT}") 63 | endif() 64 | 65 | add_compile_options(-DAK_DONT_REPLACE_STD) 66 | add_compile_options(-Wno-expansion-to-defined) 67 | add_compile_options(-Wno-user-defined-literals) 68 | 69 | # Needed by Gtkmm 70 | add_compile_options(-fexceptions) 71 | 72 | set(BROWSER_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Applications/Browser/) 73 | 74 | find_package(PkgConfig REQUIRED) 75 | pkg_check_modules(GTK4 REQUIRED gtkmm-4.0) 76 | pkg_check_modules(SOUP3 REQUIRED libsoup-3.0) 77 | 78 | include_directories(${GTK4_INCLUDE_DIRS}) 79 | link_directories(${GTK4_LIBRARY_DIRS}) 80 | add_definitions(${GTK4_CFLAGS_OTHER}) 81 | 82 | include_directories(${SOUP3_INCLUDE_DIRS}) 83 | link_directories(${SOUP3_LIBRARY_DIRS}) 84 | add_definitions(${SOUP3_CFLAGS_OTHER}) 85 | 86 | add_subdirectory(src) 87 | add_subdirectory(demo) 88 | 89 | if(NOT CMAKE_SKIP_INSTALL_RULES) 90 | include(cmake/InstallRules.cmake) 91 | endif() 92 | 93 | include(CTest) 94 | #if (BUILD_TESTING) 95 | # add_test( 96 | # NAME LibWeb 97 | # COMMAND ${CMAKE_CURRENT_BINARY_DIR}/../bin/headless-browser --run-tests ${SERENITY_SOURCE_DIR}/Tests/LibWeb 98 | # ) 99 | # set_tests_properties(LibWeb PROPERTIES ENVIRONMENT QT_QPA_PLATFORM=offscreen) 100 | #endif() 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018-2023, the SerenityOS developers. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibWeb for GTK 2 | LibWeb is the SerenityOS Browser Engine that powers the ladybird browser. 3 | 4 | It's a new, independent, and portable browser engine that is making incredible progress. 5 | 6 | LibWebGTK is akin to WebKitGTK in that it wraps LibWeb for embedding inside GTK based applications. 7 | 8 | ![A screenshot of the LibWebGTK sample browser on the Ladybird Browser announcement blog post](screenshot.png) 9 | 10 | ## Status 11 | This is not production ready so use at your own risk. 12 | 13 | ## Contributing 14 | Interested in contributing? Start a discussion and let's get in touch! 15 | 16 | ## Building 17 | Clone the repo and check out submodules 18 | ``` 19 | git clone ... 20 | git submodule update --init --recursive 21 | ``` 22 | 23 | ### Command Line 24 | Make a new directory `Build` in the repository root. 25 | 26 | From inside that directory, run: 27 | 28 | ``` 29 | cmake .. -GNinja 30 | ninja run demo-browser 31 | ``` 32 | 33 | ### CLion Setup 34 | Open the project in CLion and load the CMake file in the repository root. 35 | 36 | Find the run configuration named `run`, edit it, set `demo-browser` as the executable. 37 | 38 | You are now ready to build and run `libweb-gtk`. 39 | 40 | ### macOS 41 | On macOS, you will also need `icu4c` in your path. Set the following environment variable 42 | 43 | ``` 44 | # Intel 45 | PKG_CONFIG_PATH="/usr/local/opt/icu4c/lib/pkgconfig" 46 | 47 | # M1 48 | PKG_CONFIG_PATH="/opt/homebrew/opt/icu4c/lib/pkgconfig" 49 | ``` 50 | 51 | You can do this in your CMake profile in CLion, or as part of your `.zshrc`. 52 | 53 | See: https://www.jetbrains.com/help/clion/cmake-profile.html#EnvVariables 54 | 55 | #### Debugging 56 | On macOS, there is no gdb. Instead, use `ninja debug-lldb`. 57 | 58 | ## Licence 59 | Available under the same licence as SerenityOS (BSD 2-Clause). -------------------------------------------------------------------------------- /cmake/EnableLLD.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Andrew Kaster 2 | # 3 | # SPDX-License-Identifier: BSD-2-Clause 4 | # 5 | option(LADYBIRD_USE_LLD "Use llvm lld to link application" ON) 6 | if (LADYBIRD_USE_LLD AND NOT APPLE) 7 | find_program(LLD_LINKER NAMES "ld.lld") 8 | if (NOT LLD_LINKER) 9 | message(INFO "LLD not found, cannot use to link. Disabling option...") 10 | set(LADYBIRD_USE_LLD OFF CACHE BOOL "" FORCE) 11 | endif() 12 | endif() 13 | if (LADYBIRD_USE_LLD AND NOT APPLE) 14 | add_link_options(-fuse-ld=lld) 15 | add_compile_options(-ggnu-pubnames) 16 | add_link_options(LINKER:--gdb-index) 17 | endif() 18 | -------------------------------------------------------------------------------- /cmake/EnableLagom.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Andrew Kaster 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | set(BUILD_LAGOM ON CACHE INTERNAL "Build all Lagom targets") 6 | 7 | set(LAGOM_SOURCE_DIR "${SERENITY_SOURCE_DIR}/Meta/Lagom") 8 | set(LAGOM_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/Lagom") 9 | 10 | # FIXME: Setting target_include_directories on Lagom libraries might make this unnecessary? 11 | include_directories(${SERENITY_SOURCE_DIR}) 12 | include_directories(${SERENITY_SOURCE_DIR}/Userland/Services) 13 | include_directories(${SERENITY_SOURCE_DIR}/Userland/Libraries) 14 | include_directories(${LAGOM_BINARY_DIR}) 15 | include_directories(${LAGOM_BINARY_DIR}/Userland/Services) 16 | include_directories(${LAGOM_BINARY_DIR}/Userland/Libraries) 17 | 18 | # We set EXCLUDE_FROM_ALL to make sure that only required Lagom libraries are built 19 | add_subdirectory("${LAGOM_SOURCE_DIR}" "${LAGOM_BINARY_DIR}" EXCLUDE_FROM_ALL) 20 | -------------------------------------------------------------------------------- /cmake/InstallRules.cmake: -------------------------------------------------------------------------------- 1 | 2 | include(CMakePackageConfigHelpers) 3 | include(GNUInstallDirs) 4 | 5 | set(package WebEmbed) 6 | 7 | #set(webembed_applications webembed SQLServer WebContent WebDriver headless-browser) 8 | set(webembed_applications demo-browser SQLServer WebContent WebDriver) 9 | 10 | install(TARGETS ${webembed_applications} 11 | EXPORT webembedTargets 12 | RUNTIME 13 | COMPONENT webembed_Runtime 14 | DESTINATION ${CMAKE_INSTALL_BINDIR} 15 | BUNDLE 16 | COMPONENT webembed_Runtime 17 | DESTINATION bundle 18 | LIBRARY 19 | COMPONENT webembed_Runtime 20 | NAMELINK_COMPONENT webembed_Development 21 | DESTINATION ${CMAKE_INSTALL_LIBDIR} 22 | ) 23 | 24 | include("${SERENITY_SOURCE_DIR}/Meta/Lagom/get_linked_lagom_libraries.cmake") 25 | foreach (application IN LISTS webembed_applications) 26 | get_linked_lagom_libraries("${application}" "${application}_lagom_libraries") 27 | list(APPEND all_required_lagom_libraries "${${application}_lagom_libraries}") 28 | endforeach() 29 | list(REMOVE_DUPLICATES all_required_lagom_libraries) 30 | 31 | install(TARGETS ${all_required_lagom_libraries} 32 | EXPORT webembedTargets 33 | COMPONENT webembed_Runtime 34 | LIBRARY 35 | COMPONENT webembed_Runtime 36 | NAMELINK_COMPONENT webembed_Development 37 | DESTINATION ${CMAKE_INSTALL_LIBDIR} 38 | ) 39 | 40 | write_basic_package_version_file( 41 | "${package}ConfigVersion.cmake" 42 | COMPATIBILITY SameMajorVersion 43 | ) 44 | 45 | # Allow package maintainers to freely override the path for the configs 46 | set( 47 | webembed_INSTALL_CMAKEDIR "${CMAKE_INSTALL_DATADIR}/${package}" 48 | CACHE PATH "CMake package config location relative to the install prefix" 49 | ) 50 | mark_as_advanced(webembed_INSTALL_CMAKEDIR) 51 | 52 | install( 53 | FILES cmake/LadybirdInstallConfig.cmake 54 | DESTINATION "${webembed_INSTALL_CMAKEDIR}" 55 | RENAME "${package}Config.cmake" 56 | COMPONENT webembed_Development 57 | ) 58 | 59 | install( 60 | FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake" 61 | DESTINATION "${webembed_INSTALL_CMAKEDIR}" 62 | COMPONENT webembed_Development 63 | ) 64 | 65 | install( 66 | EXPORT webembedTargets 67 | NAMESPACE webembed:: 68 | DESTINATION "${webembed_INSTALL_CMAKEDIR}" 69 | COMPONENT webembed_Development 70 | ) 71 | 72 | install(DIRECTORY 73 | "${SERENITY_SOURCE_DIR}/Base/res/html" 74 | "${SERENITY_SOURCE_DIR}/Base/res/fonts" 75 | "${SERENITY_SOURCE_DIR}/Base/res/icons" 76 | "${SERENITY_SOURCE_DIR}/Base/res/themes" 77 | "${SERENITY_SOURCE_DIR}/Base/res/color-palettes" 78 | "${SERENITY_SOURCE_DIR}/Base/res/cursor-themes" 79 | DESTINATION "${CMAKE_INSTALL_DATADIR}/res" 80 | USE_SOURCE_PERMISSIONS MESSAGE_NEVER 81 | COMPONENT webembed_Runtime 82 | ) 83 | 84 | install(FILES 85 | "${SERENITY_SOURCE_DIR}/Base/home/anon/.config/BrowserAutoplayAllowlist.txt" 86 | "${SERENITY_SOURCE_DIR}/Base/home/anon/.config/BrowserContentFilters.txt" 87 | DESTINATION "${CMAKE_INSTALL_DATADIR}/res/webembed" 88 | COMPONENT webembed_Runtime 89 | ) 90 | -------------------------------------------------------------------------------- /cmake/LadybirdInstallConfig.cmake: -------------------------------------------------------------------------------- 1 | include("${CMAKE_CURRENT_LIST_DIR}/ladybirdTargets.cmake") 2 | -------------------------------------------------------------------------------- /demo/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES 2 | main.c 3 | ) 4 | 5 | add_executable(demo-browser ${SOURCES}) 6 | 7 | add_dependencies(demo-browser webembed SQLServer WebContent WebDriver) 8 | 9 | target_include_directories(demo-browser PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 10 | target_include_directories(demo-browser PRIVATE ${SERENITY_SOURCE_DIR}/Userland/) 11 | target_include_directories(demo-browser PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Applications/) 12 | target_include_directories(demo-browser PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/) 13 | 14 | target_link_libraries(demo-browser PRIVATE ${GTK4_LIBRARIES} webembed LibWeb LibWebView LibWebSocket LibCrypto LibFileSystem LibGemini LibHTTP LibJS LibGfx LibMain LibTLS LibIPC LibJS LibDiff LibSQL) 15 | 16 | set_target_properties(demo-browser PROPERTIES 17 | MACOSX_BUNDLE_GUI_IDENTIFIER com.mattjakeman.LibWebGTK 18 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 19 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 20 | MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist" 21 | MACOSX_BUNDLE TRUE 22 | WIN32_EXECUTABLE TRUE 23 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER com.mattjakeman.LibWebGTK 24 | ) 25 | 26 | if (APPLE) 27 | # FIXME: Create a proper app bundle for each helper process 28 | set(app_dir "$") 29 | set(bundle_dir "$") 30 | add_custom_command(TARGET demo-browser POST_BUILD 31 | COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${app_dir}" 32 | COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${app_dir}" 33 | COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" "${app_dir}" 34 | ) 35 | endif() -------------------------------------------------------------------------------- /demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrincipalClass 6 | NSApplication 7 | CFBundleIconFile 8 | 9 | CFBundlePackageType 10 | APPL 11 | CFBundleGetInfoString 12 | Ladybird 13 | CFBundleSignature 14 | 15 | CFBundleExecutable 16 | ladybird 17 | CFBundleIdentifier 18 | org.SerenityOS.Ladybird 19 | NSPrincipalClass 20 | NSApplication 21 | NSHighResolutionCapable 22 | True 23 | 24 | 25 | -------------------------------------------------------------------------------- /demo/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "Embed/webembed.h" 8 | #include "Embed/webcontentview.h" 9 | 10 | static void on_activate (GtkApplication *app) { 11 | 12 | GtkWidget *window = gtk_application_window_new(app); 13 | gtk_window_set_default_size(GTK_WINDOW (window), 720, 480); 14 | gtk_window_set_title(GTK_WINDOW (window), "LibWebGTK"); 15 | 16 | GtkWidget *view_left = web_content_view_new (); 17 | GtkWidget *view_right = web_content_view_new (); 18 | 19 | GtkWidget *navigation = gtk_entry_new(); 20 | gtk_widget_set_hexpand(navigation, true); 21 | 22 | GtkWidget *button = gtk_button_new_with_label ("Go!"); 23 | // button.signal_clicked().connect([&]() { 24 | // auto url = navigation.get_buffer().get()->get_text(); 25 | // web_content_view_load(WEB_CONTENT_VIEW(view), url.c_str()); 26 | // }); 27 | 28 | GtkWidget *controls = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); 29 | gtk_widget_add_css_class (controls, "toolbar"); 30 | gtk_box_append(GTK_BOX (controls), navigation); 31 | gtk_box_append(GTK_BOX (controls), button); 32 | 33 | GtkWidget *paned = gtk_paned_new (GTK_ORIENTATION_HORIZONTAL); 34 | 35 | GtkWidget *scroll_area_left = gtk_scrolled_window_new(); 36 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW(scroll_area_left), view_left); 37 | gtk_widget_set_vexpand (GTK_WIDGET(scroll_area_left), TRUE); 38 | gtk_paned_set_start_child (GTK_PANED (paned), scroll_area_left); 39 | 40 | GtkWidget *scroll_area_right = gtk_scrolled_window_new(); 41 | gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW(scroll_area_right), view_right); 42 | gtk_widget_set_vexpand (GTK_WIDGET(scroll_area_right), TRUE); 43 | gtk_paned_set_end_child (GTK_PANED (paned), scroll_area_right); 44 | 45 | GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 46 | gtk_box_append(GTK_BOX (box), controls); 47 | gtk_box_append(GTK_BOX (box), paned); 48 | 49 | gtk_window_set_child(GTK_WINDOW (window), GTK_WIDGET (box)); 50 | gtk_window_present(GTK_WINDOW(window)); 51 | 52 | web_content_view_load(WEB_CONTENT_VIEW(view_left), "https://mattjakeman.com/"); 53 | web_content_view_load(WEB_CONTENT_VIEW(view_right), "https://awesomekling.github.io/Ladybird-a-new-cross-platform-browser-project/"); 54 | } 55 | 56 | int main(int argc, char **argv) 57 | { 58 | GtkApplication *app; 59 | 60 | app = gtk_application_new("com.mattjakeman.LibWebGTK", G_APPLICATION_DEFAULT_FLAGS); 61 | 62 | gtk_init(); 63 | web_embed_init(); 64 | 65 | g_signal_connect(app, "activate", G_CALLBACK (on_activate), NULL); 66 | 67 | return g_application_run(G_APPLICATION(app), argc, argv); 68 | } 69 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | Here are examples of using LibWebGTK from other languages. 3 | 4 | To run the python demo, you'll first need it in the same directory as `libwebembed.lib`. 5 | 6 | This is so it can find the web process binary. 7 | 8 | ```bash 9 | # assume your cmake build directory is called _build 10 | cp example.py ../_build/src 11 | 12 | # go to directory 13 | cd ../_build/src 14 | 15 | # set env variables 16 | # gobject introspection 17 | export GI_TYPELIB_PATH=`pwd` 18 | 19 | # serenity resources 20 | export SERENITY_SOURCE_DIR=`pwd`/../../serenity 21 | 22 | # run 23 | python3 example.py 24 | ``` -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import gi 3 | 4 | gi.require_version("Gtk", "4.0") 5 | gi.require_version("Web", "0.0") 6 | from gi.repository import GLib, Gtk, Web 7 | 8 | 9 | class MyApplication(Gtk.Application): 10 | def __init__(self): 11 | super().__init__(application_id="com.mattjakeman.LibWebGTK.Demo") 12 | GLib.set_application_name('LibWebGTK Python Demo') 13 | 14 | def do_activate(self): 15 | window = Gtk.ApplicationWindow(application=self, title="LibWeb GTK Demo") 16 | window.set_default_size(720, 480) 17 | 18 | Web.embed_init() 19 | 20 | web = Web.ContentView() 21 | web.load("https://mattjakeman.com") 22 | 23 | scroll_area = Gtk.ScrolledWindow() 24 | scroll_area.set_child(web) 25 | 26 | window.set_child(scroll_area) 27 | window.present() 28 | 29 | 30 | app = MyApplication() 31 | exit_status = app.run(sys.argv) 32 | sys.exit(exit_status) 33 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjakeman/libweb-gtk/c60082935a5af1ffca405b15473d82ca0c8863cb/screenshot.png -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | .qmake.stash 2 | Makefile 3 | ladybird 4 | *.o 5 | moc_* 6 | Build 7 | build 8 | CMakeLists.txt.user 9 | android/gradle 10 | android/gradlew* 11 | android/assets/ 12 | 13 | -------------------------------------------------------------------------------- /src/AudioCodecPluginLadybird.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Tim Flynn 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "AudioCodecPluginLadybird.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace Ladybird { 15 | 16 | static constexpr u32 UPDATE_RATE_MS = 10; 17 | 18 | struct AudioTask { 19 | enum class Type { 20 | Stop, 21 | Play, 22 | Pause, 23 | Seek, 24 | Volume, 25 | RecreateAudioDevice, 26 | }; 27 | 28 | Type type; 29 | Optional data {}; 30 | }; 31 | 32 | using AudioTaskQueue = Core::SharedSingleProducerCircularQueue; 33 | 34 | class AudioThread final { // We have to use QThread, otherwise internal Qt media QTimer objects do not work. 35 | 36 | public: 37 | static ErrorOr> create(NonnullRefPtr loader) 38 | { 39 | auto task_queue = TRY(AudioTaskQueue::create()); 40 | return adopt_nonnull_own_or_enomem(new (nothrow) AudioThread(move(loader), move(task_queue))); 41 | } 42 | 43 | ErrorOr stop() 44 | { 45 | TRY(queue_task({ AudioTask::Type::Stop })); 46 | // wait(); 47 | 48 | return {}; 49 | } 50 | 51 | Duration duration() const 52 | { 53 | return m_duration; 54 | } 55 | 56 | ErrorOr queue_task(AudioTask task) 57 | { 58 | return m_task_queue.blocking_enqueue(move(task), []() { 59 | usleep(UPDATE_RATE_MS * 1000); 60 | }); 61 | } 62 | 63 | //Q_SIGNALS: 64 | // void playback_position_updated(Duration); 65 | 66 | private: 67 | AudioThread(NonnullRefPtr loader, AudioTaskQueue task_queue) 68 | : m_loader(move(loader)) 69 | , m_task_queue(move(task_queue)) 70 | { 71 | auto duration = static_cast(m_loader->total_samples()) / static_cast(m_loader->sample_rate()); 72 | m_duration = Duration::from_milliseconds(static_cast(duration * 1000.0)); 73 | } 74 | // 75 | // enum class Paused { 76 | // Yes, 77 | // No, 78 | // }; 79 | // 80 | //struct AudioDevice { 81 | // static AudioDevice create(Audio::Loader const& loader) 82 | // { 83 | // auto const& device_info = QMediaDevices::defaultAudioOutput(); 84 | // 85 | // auto format = device_info.preferredFormat(); 86 | // format.setSampleRate(static_cast(loader.sample_rate())); 87 | // format.setChannelCount(2); 88 | // 89 | // auto audio_output = make(device_info, format); 90 | // return AudioDevice { move(audio_output) }; 91 | // } 92 | // 93 | // AudioDevice(AudioDevice&&) = default; 94 | // 95 | // AudioDevice& operator=(AudioDevice&& device) 96 | // { 97 | // if (audio_output) { 98 | // audio_output->stop(); 99 | // io_device = nullptr; 100 | // } 101 | // 102 | // swap(audio_output, device.audio_output); 103 | // swap(io_device, device.io_device); 104 | // return *this; 105 | // } 106 | // 107 | // ~AudioDevice() 108 | // { 109 | // if (audio_output) 110 | // audio_output->stop(); 111 | // } 112 | // 113 | // OwnPtr audio_output; 114 | // QIODevice* io_device { nullptr }; 115 | // 116 | // private: 117 | // explicit AudioDevice(NonnullOwnPtr output) 118 | // : audio_output(move(output)) 119 | // { 120 | // io_device = audio_output->start(); 121 | // } 122 | // }; 123 | 124 | // void run() override 125 | // { 126 | // auto devices = make(); 127 | // auto audio_device = AudioDevice::create(m_loader); 128 | // 129 | // connect(devices, &QMediaDevices::audioOutputsChanged, this, [this]() { 130 | // queue_task({ AudioTask::Type::RecreateAudioDevice }).release_value_but_fixme_should_propagate_errors(); 131 | // }); 132 | // 133 | // auto paused = Paused::Yes; 134 | // 135 | // while (true) { 136 | // auto& audio_output = audio_device.audio_output; 137 | // auto* io_device = audio_device.io_device; 138 | // 139 | // if (auto result = m_task_queue.dequeue(); result.is_error()) { 140 | // VERIFY(result.error() == AudioTaskQueue::QueueStatus::Empty); 141 | // } else { 142 | // auto task = result.release_value(); 143 | // 144 | // switch (task.type) { 145 | // case AudioTask::Type::Stop: 146 | // return; 147 | // 148 | // case AudioTask::Type::Play: 149 | // audio_output->resume(); 150 | // paused = Paused::No; 151 | // break; 152 | // 153 | // case AudioTask::Type::Pause: 154 | // audio_output->suspend(); 155 | // paused = Paused::Yes; 156 | // break; 157 | // 158 | // case AudioTask::Type::Seek: 159 | // VERIFY(task.data.has_value()); 160 | // m_position = Web::Platform::AudioCodecPlugin::set_loader_position(m_loader, *task.data, m_duration, audio_output->format().sampleRate()); 161 | // 162 | // if (paused == Paused::Yes) 163 | // Q_EMIT playback_position_updated(m_position); 164 | // 165 | // break; 166 | // 167 | // case AudioTask::Type::Volume: 168 | // VERIFY(task.data.has_value()); 169 | // audio_output->setVolume(*task.data); 170 | // break; 171 | // 172 | // case AudioTask::Type::RecreateAudioDevice: 173 | // audio_device = AudioDevice::create(m_loader); 174 | // continue; 175 | // } 176 | // } 177 | // 178 | // if (paused == Paused::No) { 179 | // if (auto result = play_next_samples(*audio_output, *io_device); result.is_error()) { 180 | // // FIXME: Propagate the error to the HTMLMediaElement. 181 | // } else { 182 | // Q_EMIT playback_position_updated(m_position); 183 | // paused = result.value(); 184 | // } 185 | // } 186 | // 187 | // usleep(UPDATE_RATE_MS * 1000); 188 | // } 189 | // } 190 | // 191 | // ErrorOr play_next_samples(QAudioSink& audio_output, QIODevice& io_device) 192 | // { 193 | // bool all_samples_loaded = m_loader->loaded_samples() >= m_loader->total_samples(); 194 | // 195 | // if (all_samples_loaded) { 196 | // audio_output.suspend(); 197 | // (void)m_loader->reset(); 198 | // 199 | // m_position = m_duration; 200 | // return Paused::Yes; 201 | // } 202 | // 203 | // auto bytes_available = audio_output.bytesFree(); 204 | // auto bytes_per_sample = audio_output.format().bytesPerSample(); 205 | // auto channel_count = audio_output.format().channelCount(); 206 | // auto samples_to_load = bytes_available / bytes_per_sample / channel_count; 207 | // 208 | // auto samples = TRY(Web::Platform::AudioCodecPlugin::read_samples_from_loader(*m_loader, samples_to_load, audio_output.format().sampleRate())); 209 | // enqueue_samples(audio_output, io_device, move(samples)); 210 | // 211 | // m_position = Web::Platform::AudioCodecPlugin::current_loader_position(m_loader, audio_output.format().sampleRate()); 212 | // return Paused::No; 213 | // } 214 | // 215 | // void enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray samples) 216 | // { 217 | // auto buffer_size = samples.size() * audio_output.format().bytesPerSample() * audio_output.format().channelCount(); 218 | // 219 | // if (buffer_size > static_cast(m_sample_buffer.size())) 220 | // m_sample_buffer.resize(buffer_size); 221 | // 222 | // FixedMemoryStream stream { Bytes { m_sample_buffer.data(), buffer_size } }; 223 | // 224 | // for (auto const& sample : samples) { 225 | // switch (audio_output.format().sampleFormat()) { 226 | // case QAudioFormat::UInt8: 227 | // write_sample(stream, sample.left); 228 | // write_sample(stream, sample.right); 229 | // break; 230 | // case QAudioFormat::Int16: 231 | // write_sample(stream, sample.left); 232 | // write_sample(stream, sample.right); 233 | // break; 234 | // case QAudioFormat::Int32: 235 | // write_sample(stream, sample.left); 236 | // write_sample(stream, sample.right); 237 | // break; 238 | // case QAudioFormat::Float: 239 | // write_sample(stream, sample.left); 240 | // write_sample(stream, sample.right); 241 | // break; 242 | // default: 243 | // VERIFY_NOT_REACHED(); 244 | // } 245 | // } 246 | // 247 | // io_device.write(m_sample_buffer.data(), buffer_size); 248 | // } 249 | // 250 | // template 251 | // void write_sample(FixedMemoryStream& stream, float sample) 252 | // { 253 | // // The values that need to be written to the stream vary depending on the output channel format, and isn't 254 | // // particularly well documented. The value derivations performed below were adapted from a Qt example: 255 | // // https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/audiooutput/audiooutput.cpp?h=6.4.2#n46 256 | // LittleEndian pcm; 257 | // 258 | // if constexpr (IsSame) 259 | // pcm = static_cast((sample + 1.0f) / 2 * NumericLimits::max()); 260 | // else if constexpr (IsSame) 261 | // pcm = static_cast(sample * NumericLimits::max()); 262 | // else if constexpr (IsSame) 263 | // pcm = static_cast(sample * NumericLimits::max()); 264 | // else if constexpr (IsSame) 265 | // pcm = sample; 266 | // else 267 | // static_assert(DependentFalse); 268 | // 269 | // MUST(stream.write_value(pcm)); 270 | // } 271 | // 272 | NonnullRefPtr m_loader; 273 | AudioTaskQueue m_task_queue; 274 | // 275 | // QByteArray m_sample_buffer; 276 | // 277 | Duration m_duration; 278 | Duration m_position; 279 | }; 280 | 281 | ErrorOr> AudioCodecPluginLadybird::create(NonnullRefPtr loader) 282 | { 283 | auto audio_thread = TRY(AudioThread::create(move(loader))); 284 | // audio_thread->start(); 285 | 286 | return adopt_nonnull_own_or_enomem(new (nothrow) AudioCodecPluginLadybird(move(audio_thread))); 287 | } 288 | 289 | AudioCodecPluginLadybird::AudioCodecPluginLadybird(NonnullOwnPtr audio_thread) 290 | : m_audio_thread(move(audio_thread)) 291 | { 292 | /*connect(m_audio_thread, &AudioThread::playback_position_updated, this, [this](auto position) { 293 | if (on_playback_position_updated) 294 | on_playback_position_updated(position); 295 | });*/ 296 | } 297 | 298 | AudioCodecPluginLadybird::~AudioCodecPluginLadybird() 299 | { 300 | m_audio_thread->stop().release_value_but_fixme_should_propagate_errors(); 301 | } 302 | 303 | void AudioCodecPluginLadybird::resume_playback() 304 | { 305 | m_audio_thread->queue_task({ AudioTask::Type::Play }).release_value_but_fixme_should_propagate_errors(); 306 | } 307 | 308 | void AudioCodecPluginLadybird::pause_playback() 309 | { 310 | m_audio_thread->queue_task({ AudioTask::Type::Pause }).release_value_but_fixme_should_propagate_errors(); 311 | } 312 | 313 | void AudioCodecPluginLadybird::set_volume(double volume) 314 | { 315 | 316 | AudioTask task { AudioTask::Type::Volume }; 317 | task.data = volume; 318 | 319 | m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors(); 320 | } 321 | 322 | void AudioCodecPluginLadybird::seek(double position) 323 | { 324 | AudioTask task { AudioTask::Type::Seek }; 325 | task.data = position; 326 | 327 | m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors(); 328 | } 329 | 330 | Duration AudioCodecPluginLadybird::duration() 331 | { 332 | return m_audio_thread->duration(); 333 | } 334 | 335 | } 336 | 337 | // #include "AudioCodecPluginLadybird.moc" 338 | -------------------------------------------------------------------------------- /src/AudioCodecPluginLadybird.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Tim Flynn 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | // #include 15 | 16 | namespace Ladybird { 17 | 18 | class AudioThread; 19 | 20 | class AudioCodecPluginLadybird final 21 | : public Web::Platform::AudioCodecPlugin { 22 | 23 | public: 24 | static ErrorOr> create(NonnullRefPtr); 25 | virtual ~AudioCodecPluginLadybird() override; 26 | 27 | virtual void resume_playback() override; 28 | virtual void pause_playback() override; 29 | virtual void set_volume(double) override; 30 | virtual void seek(double) override; 31 | 32 | virtual Duration duration() override; 33 | 34 | private: 35 | explicit AudioCodecPluginLadybird(NonnullOwnPtr); 36 | 37 | NonnullOwnPtr m_audio_thread; 38 | }; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/BrowserWindow.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * Copyright (c) 2023, Linus Groh 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "Tab.h" 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | class WebContentView; 21 | 22 | namespace Browser { 23 | class CookieJar; 24 | } 25 | 26 | class BrowserWindow : public QMainWindow { 27 | Q_OBJECT 28 | public: 29 | explicit BrowserWindow(Browser::CookieJar&, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode); 30 | 31 | WebContentView& view() const { return m_current_tab->view(); } 32 | 33 | int tab_index(Tab*); 34 | 35 | QAction& go_back_action() 36 | { 37 | return *m_go_back_action; 38 | } 39 | 40 | QAction& go_forward_action() 41 | { 42 | return *m_go_forward_action; 43 | } 44 | 45 | QAction& reload_action() 46 | { 47 | return *m_reload_action; 48 | } 49 | 50 | QAction& copy_selection_action() 51 | { 52 | return *m_copy_selection_action; 53 | } 54 | 55 | QAction& select_all_action() 56 | { 57 | return *m_select_all_action; 58 | } 59 | 60 | QAction& view_source_action() 61 | { 62 | return *m_view_source_action; 63 | } 64 | 65 | QAction& inspect_dom_node_action() 66 | { 67 | return *m_inspect_dom_node_action; 68 | } 69 | 70 | public slots: 71 | void tab_title_changed(int index, QString const&); 72 | void tab_favicon_changed(int index, QIcon icon); 73 | Tab& new_tab(QString const&, Web::HTML::ActivateTab); 74 | void activate_tab(int index); 75 | void close_tab(int index); 76 | void close_current_tab(); 77 | void open_next_tab(); 78 | void open_previous_tab(); 79 | void open_file(); 80 | void enable_auto_color_scheme(); 81 | void enable_light_color_scheme(); 82 | void enable_dark_color_scheme(); 83 | void zoom_in(); 84 | void zoom_out(); 85 | void reset_zoom(); 86 | void select_all(); 87 | void copy_selected_text(); 88 | 89 | protected: 90 | bool eventFilter(QObject* obj, QEvent* event) override; 91 | 92 | private: 93 | virtual void resizeEvent(QResizeEvent*) override; 94 | virtual void moveEvent(QMoveEvent*) override; 95 | 96 | void debug_request(DeprecatedString const& request, DeprecatedString const& argument = ""); 97 | 98 | void set_current_tab(Tab* tab); 99 | void update_displayed_zoom_level(); 100 | 101 | QTabWidget* m_tabs_container { nullptr }; 102 | Vector> m_tabs; 103 | Tab* m_current_tab { nullptr }; 104 | QMenu* m_zoom_menu { nullptr }; 105 | 106 | OwnPtr m_go_back_action {}; 107 | OwnPtr m_go_forward_action {}; 108 | OwnPtr m_reload_action {}; 109 | OwnPtr m_copy_selection_action {}; 110 | OwnPtr m_select_all_action {}; 111 | OwnPtr m_view_source_action {}; 112 | OwnPtr m_inspect_dom_node_action {}; 113 | 114 | Browser::CookieJar& m_cookie_jar; 115 | 116 | StringView m_webdriver_content_ipc_path; 117 | WebView::EnableCallgrindProfiling m_enable_callgrind_profiling; 118 | WebView::UseJavaScriptBytecode m_use_javascript_bytecode; 119 | }; 120 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOURCES 2 | ${BROWSER_SOURCE_DIR}/CookieJar.cpp 3 | ${BROWSER_SOURCE_DIR}/Database.cpp 4 | ${BROWSER_SOURCE_DIR}/History.cpp 5 | # BrowserWindow.cpp 6 | # ConsoleWidget.cpp 7 | EventLoopImplementationGLib.cpp 8 | EventLoopImplementationGtk.cpp 9 | HelperProcess.cpp 10 | # InspectorWidget.cpp 11 | # LocationEdit.cpp 12 | # ModelTranslator.cpp 13 | # Settings.cpp 14 | # SettingsDialog.cpp 15 | # Tab.cpp 16 | Utilities.cpp 17 | # WebContentView.cpp 18 | ContentViewImpl.cpp 19 | 20 | Embed/webcontentview.cpp 21 | Embed/webembed.cpp 22 | ) 23 | 24 | set(EMBED 25 | "Embed/webcontentview.h" 26 | "Embed/webembed.h" 27 | ) 28 | 29 | add_library(webembed ${SOURCES}) 30 | 31 | set(DEPS ${GTK4_LIBRARIES} LibCore LibFileSystem LibGfx LibGUI LibIPC LibJS LibMain LibWeb LibWebView LibSQL LibWebSocket LibCrypto LibGemini LibHTTP LibTLS LibDiff) 32 | target_link_libraries(webembed PRIVATE ${DEPS}) 33 | 34 | foreach(dir IN LISTS INCLUDE_DIRS) 35 | target_include_directories(webembed PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) 36 | target_include_directories(webembed PRIVATE ${SERENITY_SOURCE_DIR}/Userland/) 37 | target_include_directories(webembed PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Applications/) 38 | target_include_directories(webembed PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/) 39 | endforeach() 40 | 41 | target_include_directories(webembed PUBLIC 42 | $ 43 | $ 44 | ) 45 | 46 | add_subdirectory(SQLServer) 47 | add_subdirectory(WebContent) 48 | add_subdirectory(WebDriver) 49 | 50 | add_dependencies(webembed SQLServer WebContent WebDriver) 51 | 52 | # Loosely inspired by HarfBuzz's Build System 53 | # Licensed under the "Old" MIT License 54 | set(INTROSPECTION ON) 55 | if (INTROSPECTION) 56 | find_package(PkgConfig) 57 | pkg_check_modules(GOBJECT_INTROSPECTION QUIET gobject-introspection-1.0) 58 | 59 | find_program(G_IR_SCANNER g-ir-scanner 60 | HINTS ${PC_g_ir_scanner} 61 | ) 62 | 63 | find_program(G_IR_COMPILER g-ir-compiler 64 | HINTS ${PC_g_ir_compiler} 65 | ) 66 | 67 | set(web_libpath "$") 68 | 69 | add_custom_command ( 70 | TARGET webembed 71 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 72 | POST_BUILD 73 | COMMAND "${G_IR_SCANNER}" 74 | --warn-all --no-libtool --verbose 75 | --namespace=Web 76 | --nsversion=0.0 77 | --identifier-prefix=Web 78 | --symbol-prefix=web_ 79 | --include GObject-2.0 80 | --include Gtk-4.0 81 | --pkg-export=webembed 82 | --library=webembed 83 | -L${web_libpath} 84 | ${GTK4_CFLAGS} 85 | ${EMBED} 86 | -o ${web_libpath}/Web-0.0.gir 87 | DEPENDS webembed 88 | ) 89 | 90 | add_custom_command ( 91 | TARGET webembed 92 | POST_BUILD 93 | COMMAND "${G_IR_COMPILER}" 94 | --verbose --debug 95 | --includedir ${CMAKE_CURRENT_BINARY_DIR} 96 | ${web_libpath}/Web-0.0.gir 97 | -o ${web_libpath}/Web-0.0.typelib 98 | DEPENDS ${web_libpath}/Web-0.0.gir webembed 99 | ) 100 | endif () 101 | 102 | # gi-docgen generate -C config.toml /usr/share/gir-1.0/Gtk-4.0.gir 103 | 104 | if (DOCUMENTATION AND INTROSPECTION) 105 | find_package(PkgConfig) 106 | pkg_check_modules(GI_DOCGEN QUIET gi-docgen) 107 | 108 | find_program(GI_DOCGEN gi-docgen 109 | HINTS ${PC_gi_docgen} 110 | ) 111 | 112 | if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") 113 | set(EXTRA_SEARCH_PATH "/opt/homebrew/share/gir-1.0") 114 | else() 115 | set(EXTRA_SEARCH_PATH "") 116 | endif() 117 | 118 | 119 | add_custom_command ( 120 | TARGET webembed 121 | POST_BUILD 122 | COMMAND "${GI_DOCGEN}" 123 | generate 124 | -C ${CMAKE_CURRENT_SOURCE_DIR}/config.toml 125 | ${web_libpath}/Web-0.0.gir 126 | --output-dir ${CMAKE_CURRENT_BINARY_DIR}/../docs 127 | --add-include-path "${EXTRA_SEARCH_PATH}" 128 | --no-namespace-dir 129 | DEPENDS ${web_libpath}/Web-0.0.gir webembed 130 | ) 131 | 132 | endif () -------------------------------------------------------------------------------- /src/ConsoleWidget.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Hunter Salyer 3 | * Copyright (c) 2021-2022, Andreas Kling 4 | * Copyright (c) 2021, Sam Atkins 5 | * Copyright (c) 2022, the SerenityOS developers. 6 | * 7 | * SPDX-License-Identifier: BSD-2-Clause 8 | */ 9 | 10 | #include "ConsoleWidget.h" 11 | #include "Utilities.h" 12 | #include "WebContentView.h" 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | bool is_using_dark_system_theme(QWidget&); 23 | 24 | namespace Ladybird { 25 | 26 | ConsoleWidget::ConsoleWidget() 27 | { 28 | setLayout(new QVBoxLayout); 29 | 30 | m_output_view = new WebContentView({}, WebView::EnableCallgrindProfiling::No, WebView::UseJavaScriptBytecode::No); 31 | if (is_using_dark_system_theme(*this)) 32 | m_output_view->update_palette(WebContentView::PaletteMode::Dark); 33 | 34 | m_output_view->load("data:text/html,"sv); 35 | // Wait until our output WebView is loaded, and then request any messages that occurred before we existed 36 | m_output_view->on_load_finish = [this](auto&) { 37 | if (on_request_messages) 38 | on_request_messages(0); 39 | }; 40 | 41 | layout()->addWidget(m_output_view); 42 | 43 | auto* bottom_container = new QWidget(this); 44 | bottom_container->setLayout(new QHBoxLayout); 45 | 46 | layout()->addWidget(bottom_container); 47 | 48 | m_input = new QLineEdit(bottom_container); 49 | m_input->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); 50 | bottom_container->layout()->addWidget(m_input); 51 | 52 | QObject::connect(m_input, &QLineEdit::returnPressed, [this] { 53 | auto js_source = ak_deprecated_string_from_qstring(m_input->text()); 54 | 55 | if (js_source.is_whitespace()) 56 | return; 57 | 58 | m_input->clear(); 59 | 60 | print_source_line(js_source); 61 | 62 | if (on_js_input) 63 | on_js_input(js_source); 64 | }); 65 | 66 | setFocusProxy(m_input); 67 | 68 | auto* clear_button = new QPushButton(bottom_container); 69 | bottom_container->layout()->addWidget(clear_button); 70 | clear_button->setFixedSize(22, 22); 71 | clear_button->setText("X"); 72 | clear_button->setToolTip("Clear the console output"); 73 | QObject::connect(clear_button, &QPushButton::pressed, [this] { 74 | clear_output(); 75 | }); 76 | 77 | m_input->setFocus(); 78 | } 79 | 80 | void ConsoleWidget::request_console_messages() 81 | { 82 | VERIFY(!m_waiting_for_messages); 83 | VERIFY(on_request_messages); 84 | on_request_messages(m_highest_received_message_index + 1); 85 | m_waiting_for_messages = true; 86 | } 87 | 88 | void ConsoleWidget::notify_about_new_console_message(i32 message_index) 89 | { 90 | if (message_index <= m_highest_received_message_index) { 91 | dbgln("Notified about console message we already have"); 92 | return; 93 | } 94 | if (message_index <= m_highest_notified_message_index) { 95 | dbgln("Notified about console message we're already aware of"); 96 | return; 97 | } 98 | 99 | m_highest_notified_message_index = message_index; 100 | if (!m_waiting_for_messages) 101 | request_console_messages(); 102 | } 103 | 104 | void ConsoleWidget::handle_console_messages(i32 start_index, Vector const& message_types, Vector const& messages) 105 | { 106 | i32 end_index = start_index + message_types.size() - 1; 107 | if (end_index <= m_highest_received_message_index) { 108 | dbgln("Received old console messages"); 109 | return; 110 | } 111 | 112 | for (size_t i = 0; i < message_types.size(); i++) { 113 | auto& type = message_types[i]; 114 | auto& message = messages[i]; 115 | 116 | if (type == "html") { 117 | print_html(message); 118 | } else if (type == "clear") { 119 | clear_output(); 120 | } else if (type == "group") { 121 | // FIXME: Implement. 122 | } else if (type == "groupCollapsed") { 123 | // FIXME: Implement. 124 | } else if (type == "groupEnd") { 125 | // FIXME: Implement. 126 | } else { 127 | VERIFY_NOT_REACHED(); 128 | } 129 | } 130 | 131 | m_highest_received_message_index = end_index; 132 | m_waiting_for_messages = false; 133 | 134 | if (m_highest_received_message_index < m_highest_notified_message_index) 135 | request_console_messages(); 136 | } 137 | 138 | void ConsoleWidget::print_source_line(StringView source) 139 | { 140 | StringBuilder html; 141 | html.append(""sv); 142 | html.append("> "sv); 143 | html.append(""sv); 144 | 145 | html.append(JS::MarkupGenerator::html_from_source(source).release_value_but_fixme_should_propagate_errors()); 146 | 147 | print_html(html.string_view()); 148 | } 149 | 150 | void ConsoleWidget::print_html(StringView line) 151 | { 152 | StringBuilder builder; 153 | 154 | builder.append(R"~~~( 155 | var p = document.createElement("p"); 156 | p.innerHTML = ")~~~"sv); 157 | builder.append_escaped_for_json(line); 158 | builder.append(R"~~~(" 159 | document.body.appendChild(p); 160 | )~~~"sv); 161 | 162 | // FIXME: Make it scroll to the bottom, using `window.scrollTo()` in the JS above. 163 | // We used to call `m_output_view->scroll_to_bottom();` here, but that does not work because 164 | // it runs synchronously, meaning it happens before the HTML is output via IPC above. 165 | m_output_view->run_javascript(builder.string_view()); 166 | } 167 | 168 | void ConsoleWidget::clear_output() 169 | { 170 | m_output_view->run_javascript(R"~~~( 171 | document.body.innerHTML = ""; 172 | )~~~"sv); 173 | } 174 | 175 | void ConsoleWidget::reset() 176 | { 177 | clear_output(); 178 | m_highest_notified_message_index = -1; 179 | m_highest_received_message_index = -1; 180 | m_waiting_for_messages = false; 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/ConsoleWidget.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020, Hunter Salyer 3 | * Copyright (c) 2021-2022, Andreas Kling 4 | * Copyright (c) 2021, Sam Atkins 5 | * Copyright (c) 2022, the SerenityOS developers. 6 | * 7 | * SPDX-License-Identifier: BSD-2-Clause 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | class QLineEdit; 18 | class WebContentView; 19 | 20 | namespace Ladybird { 21 | 22 | class ConsoleWidget final : public QWidget { 23 | Q_OBJECT 24 | public: 25 | ConsoleWidget(); 26 | virtual ~ConsoleWidget() = default; 27 | 28 | void notify_about_new_console_message(i32 message_index); 29 | void handle_console_messages(i32 start_index, Vector const& message_types, Vector const& messages); 30 | void print_source_line(StringView); 31 | void print_html(StringView); 32 | void reset(); 33 | 34 | WebContentView& view() { return *m_output_view; } 35 | 36 | Function on_js_input; 37 | Function on_request_messages; 38 | 39 | private: 40 | void request_console_messages(); 41 | void clear_output(); 42 | 43 | WebContentView* m_output_view { nullptr }; 44 | QLineEdit* m_input { nullptr }; 45 | 46 | i32 m_highest_notified_message_index { -1 }; 47 | i32 m_highest_received_message_index { -1 }; 48 | bool m_waiting_for_messages { false }; 49 | }; 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/ContentViewImpl.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023, Andreas Kling 3 | * Copyright (c) 2023, Linus Groh 4 | * Copyright (c) 2023, Matthew Jakeman 5 | * 6 | * SPDX-License-Identifier: BSD-2-Clause 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include "Embed/webcontentview.h" 38 | 39 | namespace WebView { 40 | class WebContentClient; 41 | } 42 | 43 | using WebView::WebContentClient; 44 | 45 | class Tab; 46 | 47 | class ContentViewImpl final 48 | : public WebView::ViewImplementation { 49 | public: 50 | explicit ContentViewImpl(WebContentView *widget, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode); 51 | virtual ~ContentViewImpl() override; 52 | 53 | Function on_tab_open_request; 54 | 55 | void snapshot_vfunc(GtkSnapshot *snapshot); 56 | void size_allocate_vfunc(int width, int height, int baseline); 57 | 58 | /*virtual void paintEvent(QPaintEvent*) override; 59 | virtual void resizeEvent(QResizeEvent*) override; 60 | virtual void mouseMoveEvent(QMouseEvent*) override; 61 | virtual void mousePressEvent(QMouseEvent*) override; 62 | virtual void mouseReleaseEvent(QMouseEvent*) override; 63 | virtual void mouseDoubleClickEvent(QMouseEvent*) override; 64 | virtual void dragEnterEvent(QDragEnterEvent*) override; 65 | virtual void dropEvent(QDropEvent*) override; 66 | virtual void keyPressEvent(QKeyEvent* event) override; 67 | virtual void keyReleaseEvent(QKeyEvent* event) override; 68 | virtual void showEvent(QShowEvent*) override; 69 | virtual void hideEvent(QHideEvent*) override; 70 | virtual void focusInEvent(QFocusEvent*) override; 71 | virtual void focusOutEvent(QFocusEvent*) override; 72 | virtual bool event(QEvent*) override;*/ 73 | 74 | void show_event(); 75 | void hide_event(); 76 | void resize_event(int width, int height); 77 | 78 | ErrorOr dump_layout_tree(); 79 | 80 | void set_viewport_rect(Gfx::IntRect); 81 | void set_window_size(Gfx::IntSize); 82 | void set_window_position(Gfx::IntPoint); 83 | 84 | enum class PaletteMode { 85 | Default, 86 | Dark, 87 | }; 88 | void update_palette(PaletteMode = PaletteMode::Default); 89 | 90 | virtual void notify_server_did_layout(Badge, Gfx::IntSize content_size) override; 91 | virtual void notify_server_did_paint(Badge, i32 bitmap_id, Gfx::IntSize) override; 92 | virtual void notify_server_did_invalidate_content_rect(Badge, Gfx::IntRect const&) override; 93 | virtual void notify_server_did_change_selection(Badge) override; 94 | virtual void notify_server_did_request_cursor_change(Badge, Gfx::StandardCursor cursor) override; 95 | virtual void notify_server_did_request_scroll(Badge, i32, i32) override; 96 | virtual void notify_server_did_request_scroll_to(Badge, Gfx::IntPoint) override; 97 | virtual void notify_server_did_request_scroll_into_view(Badge, Gfx::IntRect const&) override; 98 | virtual void notify_server_did_enter_tooltip_area(Badge, Gfx::IntPoint, DeprecatedString const&) override; 99 | virtual void notify_server_did_leave_tooltip_area(Badge) override; 100 | virtual void notify_server_did_request_alert(Badge, String const& message) override; 101 | virtual void notify_server_did_request_confirm(Badge, String const& message) override; 102 | virtual void notify_server_did_request_prompt(Badge, String const& message, String const& default_) override; 103 | virtual void notify_server_did_request_set_prompt_text(Badge, String const& message) override; 104 | virtual void notify_server_did_request_accept_dialog(Badge) override; 105 | virtual void notify_server_did_request_dismiss_dialog(Badge) override; 106 | virtual void notify_server_did_request_file(Badge, DeprecatedString const& path, i32) override; 107 | virtual void notify_server_did_finish_handling_input_event(bool event_was_accepted) override; 108 | 109 | void update_viewport_rect(); 110 | 111 | private: 112 | // ^WebView::ViewImplementation 113 | virtual void create_client(WebView::EnableCallgrindProfiling = WebView::EnableCallgrindProfiling::No, WebView::UseJavaScriptBytecode = WebView::UseJavaScriptBytecode::No) override; 114 | virtual void update_zoom() override; 115 | virtual Gfx::IntRect viewport_rect() const override; 116 | virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override; 117 | virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override; 118 | 119 | // bool on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state); 120 | // void on_key_released(guint keyval, guint keycode, Gdk::ModifierType state); 121 | void on_motion(double x, double y); 122 | 123 | Glib::RefPtr m_key_controller; 124 | Glib::RefPtr m_focus_controller; 125 | Glib::RefPtr m_motion_controller; 126 | Glib::RefPtr m_click_gesture; 127 | 128 | bool on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state); 129 | void on_key_released(guint keyval, guint keycode, Gdk::ModifierType state); 130 | void on_pressed(int n_press, double x, double y); 131 | void on_release(int n_press, double x, double y); 132 | 133 | GtkAdjustment * get_horizontal_adj() const; 134 | GtkAdjustment * get_vertical_adj() const; 135 | 136 | float m_inverse_pixel_scaling_ratio { 1.0 }; 137 | bool m_should_show_line_box_borders { false }; 138 | 139 | Glib::RefPtr m_dialog; 140 | 141 | Gfx::IntRect m_viewport_rect; 142 | 143 | StringView m_webdriver_content_ipc_path; 144 | WebContentView *m_widget; 145 | }; 146 | -------------------------------------------------------------------------------- /src/Embed/webcontentview.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Matthew Jakeman 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "webcontentview.h" 8 | #include "ContentViewImpl.h" 9 | #include "Utilities.h" 10 | 11 | #include 12 | #include 13 | 14 | struct _WebContentView 15 | { 16 | GtkWidget parent_instance; 17 | std::optional view_impl; 18 | 19 | // Scrollable 20 | GtkAdjustment *hadjustment; 21 | GtkAdjustment *vadjustment; 22 | GtkScrollablePolicy hscroll_policy; 23 | GtkScrollablePolicy vscroll_policy; 24 | 25 | // Signals 26 | guint h_adj_signal; 27 | guint v_adj_signal; 28 | 29 | // Message Backlog 30 | std::optional backlog_url; 31 | }; 32 | 33 | G_DEFINE_FINAL_TYPE_WITH_CODE(WebContentView, web_content_view, GTK_TYPE_WIDGET, 34 | G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, NULL)) 35 | 36 | enum { 37 | PROP_0, 38 | PROP_HADJUSTMENT, 39 | PROP_VADJUSTMENT, 40 | PROP_HSCROLL_POLICY, 41 | PROP_VSCROLL_POLICY, 42 | N_PROPS 43 | }; 44 | 45 | //static GParamSpec *properties [N_PROPS]; 46 | 47 | GtkWidget * 48 | web_content_view_new () 49 | { 50 | return (GtkWidget *) g_object_new (WEB_TYPE_CONTENT_VIEW, 51 | NULL); 52 | } 53 | 54 | static void 55 | web_content_view_finalize (GObject *object) 56 | { 57 | // WebContentView *self = (WebContentView *)object; 58 | 59 | G_OBJECT_CLASS (web_content_view_parent_class)->finalize (object); 60 | } 61 | 62 | static void 63 | web_content_view_dispose (GObject *object) 64 | { 65 | // WebContentView *self = (WebContentView *)object; 66 | 67 | G_OBJECT_CLASS (web_content_view_parent_class)->dispose (object); 68 | } 69 | 70 | static void 71 | cb_adjustment_changed(WebContentView *self) 72 | { 73 | if (self->view_impl.has_value()) { 74 | g_object_freeze_notify(G_OBJECT (self)); 75 | self->view_impl->update_viewport_rect(); 76 | g_object_thaw_notify(G_OBJECT (self)); 77 | } 78 | } 79 | 80 | static void 81 | web_content_view_get_property (GObject *object, 82 | guint prop_id, 83 | GValue *value, 84 | GParamSpec *pspec) 85 | { 86 | WebContentView *self = WEB_CONTENT_VIEW (object); 87 | 88 | switch (prop_id) 89 | { 90 | case PROP_HADJUSTMENT: 91 | g_value_set_object (value, self->hadjustment); 92 | break; 93 | case PROP_VADJUSTMENT: 94 | g_value_set_object (value, self->vadjustment); 95 | break; 96 | case PROP_HSCROLL_POLICY: 97 | g_value_set_enum (value, self->hscroll_policy); 98 | break; 99 | case PROP_VSCROLL_POLICY: 100 | g_value_set_enum (value, self->vscroll_policy); 101 | break; 102 | default: 103 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 104 | } 105 | } 106 | 107 | static void 108 | web_content_view_set_property (GObject *object, 109 | guint prop_id, 110 | const GValue *value, 111 | GParamSpec *pspec) 112 | { 113 | WebContentView *self = WEB_CONTENT_VIEW (object); 114 | 115 | switch (prop_id) 116 | { 117 | case PROP_HADJUSTMENT: 118 | // Clear 119 | if (self->hadjustment) { 120 | g_signal_handler_disconnect(self->hadjustment, self->h_adj_signal); 121 | g_clear_object(&self->hadjustment); 122 | } 123 | 124 | // Set New 125 | self->hadjustment = GTK_ADJUSTMENT(g_value_get_object(value)); 126 | 127 | // Connect 128 | if (self->hadjustment) { 129 | g_object_ref(self->hadjustment); 130 | self->h_adj_signal = g_signal_connect_swapped(self->hadjustment, "value-changed", G_CALLBACK(cb_adjustment_changed), self); 131 | } 132 | break; 133 | case PROP_VADJUSTMENT: 134 | // Clear 135 | if (self->vadjustment) { 136 | g_signal_handler_disconnect(self->vadjustment, self->v_adj_signal); 137 | g_clear_object(&self->vadjustment); 138 | gtk_widget_queue_resize(GTK_WIDGET (self)); 139 | } 140 | 141 | // Set New 142 | self->vadjustment = GTK_ADJUSTMENT(g_value_get_object(value)); 143 | 144 | // Connect 145 | if (self->vadjustment) { 146 | g_object_ref(self->vadjustment); 147 | self->v_adj_signal = g_signal_connect_swapped(self->vadjustment, "value-changed", G_CALLBACK(cb_adjustment_changed), self); 148 | gtk_widget_queue_resize(GTK_WIDGET (self)); 149 | } 150 | break; 151 | case PROP_HSCROLL_POLICY: 152 | self->hscroll_policy = static_cast(g_value_get_enum(value)); 153 | break; 154 | case PROP_VSCROLL_POLICY: 155 | self->vscroll_policy = static_cast(g_value_get_enum(value)); 156 | break; 157 | default: 158 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); 159 | } 160 | } 161 | 162 | void 163 | web_content_view_load (WebContentView *self, const char *url) 164 | { 165 | if (self->view_impl.has_value()) { 166 | self->view_impl->load(ak_string_from_cstring(url).value()); 167 | } else { 168 | self->backlog_url = std::string(url); 169 | } 170 | } 171 | 172 | static void 173 | web_content_view_snapshot(GtkWidget *self, GtkSnapshot *snapshot) 174 | { 175 | if (WEB_CONTENT_VIEW(self)->view_impl.has_value()) { 176 | WEB_CONTENT_VIEW(self)->view_impl->snapshot_vfunc(snapshot); 177 | } 178 | } 179 | 180 | static void 181 | web_content_view_size_allocate(GtkWidget *self, int width, int height, int baseline) 182 | { 183 | if (WEB_CONTENT_VIEW(self)->view_impl.has_value()) { 184 | WEB_CONTENT_VIEW(self)->view_impl->size_allocate_vfunc(width, height, baseline); 185 | } 186 | } 187 | 188 | static void 189 | web_content_view_class_init (WebContentViewClass *klass) 190 | { 191 | GObjectClass *object_class = G_OBJECT_CLASS (klass); 192 | 193 | object_class->finalize = web_content_view_finalize; 194 | object_class->dispose = web_content_view_dispose; 195 | object_class->get_property = web_content_view_get_property; 196 | object_class->set_property = web_content_view_set_property; 197 | 198 | g_object_class_override_property (object_class, PROP_HADJUSTMENT, "hadjustment"); 199 | g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment"); 200 | g_object_class_override_property (object_class, PROP_HSCROLL_POLICY, "hscroll-policy"); 201 | g_object_class_override_property (object_class, PROP_VSCROLL_POLICY, "vscroll-policy"); 202 | 203 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); 204 | 205 | widget_class->snapshot = web_content_view_snapshot; 206 | widget_class->size_allocate = web_content_view_size_allocate; 207 | } 208 | 209 | static void 210 | cb_main_loop (WebContentView *self) 211 | { 212 | // So... what's this for? 213 | // 214 | // Basically, the WebContentView widget needs the GTK Event Loop to already be running 215 | // otherwise the web process will crash. This then corrupts the widget and breaks everything. 216 | // 217 | // So we schedule the creation of the actual ViewImplementation (and thus WebContent process) 218 | // until we know the GLib event loop is running, which achieve with g_timeout_add_once(). 219 | self->view_impl.emplace(g_object_ref(self), String(), WebView::EnableCallgrindProfiling::No, WebView::UseJavaScriptBytecode::Yes); 220 | 221 | if (self->backlog_url.has_value()) { 222 | web_content_view_load(WEB_CONTENT_VIEW(self), self->backlog_url->c_str()); 223 | self->backlog_url.reset(); 224 | } 225 | 226 | gtk_widget_queue_resize(GTK_WIDGET (self)); 227 | } 228 | 229 | static void 230 | on_realize (WebContentView *self) { 231 | // We need to wait for the type to have finished construction 232 | // This is a good a place as any 233 | g_timeout_add_once(0, reinterpret_cast(cb_main_loop), self); 234 | } 235 | 236 | static void 237 | web_content_view_init (WebContentView *self) 238 | { 239 | g_signal_connect(self, "realize", G_CALLBACK (on_realize), NULL); 240 | } 241 | -------------------------------------------------------------------------------- /src/Embed/webcontentview.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Matthew Jakeman 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | G_BEGIN_DECLS 12 | 13 | #define WEB_TYPE_CONTENT_VIEW (web_content_view_get_type()) 14 | 15 | G_DECLARE_FINAL_TYPE (WebContentView, web_content_view, WEB, CONTENT_VIEW, GtkWidget) 16 | 17 | GtkWidget * 18 | web_content_view_new (); 19 | 20 | void 21 | web_content_view_load (WebContentView *self, const char *url); 22 | 23 | G_END_DECLS 24 | -------------------------------------------------------------------------------- /src/Embed/webembed.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Matthew Jakeman 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "webembed.h" 8 | #include "LibCore/EventLoopImplementation.h" 9 | #include "EventLoopImplementationGLib.h" 10 | #include "LibCore/EventLoop.h" 11 | #include "Utilities.h" 12 | #include "LibGfx/Font/FontDatabase.h" 13 | 14 | #include 15 | 16 | static ErrorOr handle_attached_debugger() 17 | { 18 | #ifdef AK_OS_LINUX 19 | // Let's ignore SIGINT if we're being debugged because GDB 20 | // incorrectly forwards the signal to us even when it's set to 21 | // "nopass". See https://sourceware.org/bugzilla/show_bug.cgi?id=9425 22 | // for details. 23 | auto unbuffered_status_file = TRY(Core::File::open("/proc/self/status"sv, Core::File::OpenMode::Read)); 24 | auto status_file = TRY(Core::InputBufferedFile::create(move(unbuffered_status_file))); 25 | auto buffer = TRY(ByteBuffer::create_uninitialized(4096)); 26 | while (TRY(status_file->can_read_line())) { 27 | auto line = TRY(status_file->read_line(buffer)); 28 | auto const parts = line.split_view(':'); 29 | if (parts.size() < 2 || parts[0] != "TracerPid"sv) 30 | continue; 31 | auto tracer_pid = parts[1].to_uint(); 32 | if (tracer_pid != 0UL) { 33 | dbgln("Debugger is attached, ignoring SIGINT"); 34 | TRY(Core::System::signal(SIGINT, SIG_IGN)); 35 | } 36 | break; 37 | } 38 | #endif 39 | return {}; 40 | } 41 | 42 | #include "AK/Error.h" 43 | #include 44 | #include 45 | #include 46 | 47 | std::unique_ptr event_loop_ptr; 48 | 49 | void web_embed_init() 50 | { 51 | gtk_init(); 52 | Gtk::init_gtkmm_internals(); 53 | 54 | // Setup utility methods for GLib event loop integration 55 | // -> Note that EventLoopManagerGLib operates on the default event loop, so it can reuse 56 | // that of GtkApplication and work transparently 57 | // -> Theoretically, anyway... 58 | Core::EventLoopManager::install(*new Ladybird::EventLoopManagerGLib); 59 | event_loop_ptr = std::make_unique(); // Create main loop and keep it around 60 | 61 | auto _ = handle_attached_debugger(); 62 | 63 | platform_init(); 64 | 65 | // NOTE: We only instantiate this to ensure that Gfx::FontDatabase has its default queries initialized. 66 | Gfx::FontDatabase::set_default_font_query("Katica 10 400 0"); 67 | Gfx::FontDatabase::set_fixed_width_font_query("Csilla 10 400 0"); 68 | } -------------------------------------------------------------------------------- /src/Embed/webembed.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Matthew Jakeman 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | void web_embed_init(); 14 | 15 | #ifdef __cplusplus 16 | } 17 | #endif -------------------------------------------------------------------------------- /src/EventLoopImplementationGLib.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023, Andreas Kling 3 | * Copyright (c) 2023, Matthew Jakeman 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include "EventLoopImplementationGLib.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Ladybird { 17 | 18 | struct ThreadData; 19 | static thread_local ThreadData* s_thread_data; 20 | 21 | struct ThreadData { 22 | static ThreadData& the() 23 | { 24 | if (!s_thread_data) { 25 | // FIXME: Don't leak this. 26 | s_thread_data = new ThreadData; 27 | } 28 | return *s_thread_data; 29 | } 30 | 31 | IDAllocator timer_id_allocator; 32 | HashMap> timers; 33 | HashMap> notifiers; 34 | HashMap> fd_channels; 35 | }; 36 | 37 | EventLoopImplementationGLib::EventLoopImplementationGLib() 38 | { 39 | m_event_loop = g_main_loop_new(nullptr, FALSE); 40 | } 41 | 42 | EventLoopImplementationGLib::~EventLoopImplementationGLib() 43 | { 44 | g_main_loop_unref(m_event_loop); 45 | } 46 | 47 | int EventLoopImplementationGLib::exec() 48 | { 49 | g_main_loop_run(m_event_loop); 50 | return m_error_code; 51 | } 52 | 53 | size_t EventLoopImplementationGLib::pump(PumpMode mode) 54 | { 55 | auto result = Core::ThreadEventQueue::current().process(); 56 | if (mode == PumpMode::WaitForEvents) { 57 | g_main_context_iteration(g_main_loop_get_context(m_event_loop), TRUE); 58 | } else { 59 | } 60 | result += Core::ThreadEventQueue::current().process(); 61 | return result; 62 | } 63 | 64 | void EventLoopImplementationGLib::quit(int code) 65 | { 66 | m_error_code = code; 67 | g_main_loop_quit(m_event_loop); 68 | } 69 | 70 | void EventLoopImplementationGLib::wake() {} 71 | 72 | void EventLoopImplementationGLib::post_event(Core::Object& receiver, NonnullOwnPtr&& event) 73 | { 74 | // Can we have multithreaded event queues? 75 | m_thread_event_queue.post_event(receiver, move(event)); 76 | if (&m_thread_event_queue != &Core::ThreadEventQueue::current()) 77 | wake(); 78 | } 79 | 80 | static void glib_timer_fired(int timer_id, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible, Core::Object& object) 81 | { 82 | if (should_fire_when_not_visible == Core::TimerShouldFireWhenNotVisible::No) { 83 | if (!object.is_visible_for_timer_purposes()) 84 | return; 85 | } 86 | Core::TimerEvent event(timer_id); 87 | object.dispatch_event(event); 88 | } 89 | 90 | int EventLoopManagerGLib::register_timer(Core::Object& object, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible) 91 | { 92 | auto& thread_data = ThreadData::the(); 93 | 94 | auto timer_id = thread_data.timer_id_allocator.allocate(); 95 | auto weak_object = object.make_weak_ptr(); 96 | 97 | auto source = Glib::TimeoutSource::create(milliseconds); 98 | source->connect([timer_id, should_fire_when_not_visible, should_reload, weak_object = move(weak_object)]() -> bool { 99 | auto object = weak_object.strong_ref(); 100 | if (!object) 101 | return false; 102 | glib_timer_fired(timer_id, should_fire_when_not_visible, *object); 103 | return should_reload; 104 | }); 105 | source->attach(Glib::MainContext::get_default()); 106 | thread_data.timers.set(timer_id, move(source)); 107 | 108 | return timer_id; 109 | } 110 | 111 | bool EventLoopManagerGLib::unregister_timer(int timer_id) 112 | { 113 | auto& thread_data = ThreadData::the(); 114 | thread_data.timer_id_allocator.deallocate(timer_id); 115 | 116 | auto timer = thread_data.timers.get(timer_id); 117 | if (timer.has_value()) { 118 | timer->get()->destroy(); 119 | } 120 | 121 | return thread_data.timers.remove(timer_id); 122 | } 123 | 124 | void EventLoopManagerGLib::register_notifier(Core::Notifier& notifier) 125 | { 126 | Glib::IOCondition condition; 127 | switch (notifier.type()) { 128 | case Core::Notifier::Type::Read: 129 | condition = Glib::IOCondition::IO_IN; 130 | break; 131 | case Core::Notifier::Type::Write: 132 | condition = Glib::IOCondition::IO_OUT; 133 | break; 134 | default: 135 | TODO(); 136 | } 137 | 138 | auto fd = notifier.fd(); 139 | auto thread_data = ThreadData::the(); 140 | 141 | Glib::RefPtr channel; 142 | 143 | // Get existing channel 144 | auto maybe_channel = thread_data.fd_channels.get(fd); 145 | if (maybe_channel.has_value()) { 146 | channel = maybe_channel.value(); 147 | channel->reference(); 148 | } else { 149 | channel = Glib::IOChannel::create_from_fd(notifier.fd()); 150 | thread_data.fd_channels.set(fd, channel); 151 | 152 | // FIXME: Add callback to remove from hashmap 153 | // channel->add_destroy_notify_callback(nullptr, ...); 154 | } 155 | 156 | auto io_watch = channel->create_watch(condition); 157 | 158 | io_watch->connect([condition, ¬ifier](Glib::IOCondition cond) -> bool { 159 | if (cond == condition) { 160 | Core::NotifierActivationEvent event(notifier.fd()); 161 | notifier.dispatch_event(event); 162 | } 163 | return G_SOURCE_CONTINUE; 164 | }); 165 | io_watch->attach(Glib::MainContext::get_default()); 166 | 167 | // Store watch ID for later 168 | // ThreadData::the().notifiers.set(¬ifier, move(io_watch)); 169 | } 170 | 171 | void EventLoopManagerGLib::unregister_notifier(Core::Notifier& notifier) 172 | { 173 | auto thread_data = ThreadData::the(); 174 | 175 | auto watch = thread_data.notifiers.get(¬ifier); 176 | if (watch.has_value()) { 177 | watch->get()->destroy(); 178 | } 179 | 180 | auto channel = thread_data.fd_channels.get(notifier.fd()); 181 | if (channel.has_value()) { 182 | channel->get()->unreference(); 183 | } 184 | 185 | ThreadData::the().notifiers.remove(¬ifier); 186 | } 187 | 188 | void cb_process_events() { 189 | Core::ThreadEventQueue::current().process(); 190 | } 191 | 192 | void EventLoopManagerGLib::did_post_event() 193 | { 194 | g_timeout_add_once(0, reinterpret_cast(cb_process_events), nullptr); 195 | } 196 | 197 | EventLoopManagerGLib::EventLoopManagerGLib() 198 | { 199 | g_timeout_add_once(0, reinterpret_cast(cb_process_events), nullptr); 200 | } 201 | 202 | EventLoopManagerGLib::~EventLoopManagerGLib() = default; 203 | 204 | NonnullOwnPtr EventLoopManagerGLib::make_implementation() 205 | { 206 | return adopt_own(*new EventLoopImplementationGLib); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /src/EventLoopImplementationGLib.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023, Andreas Kling 3 | * Copyright (c) 2023, Matthew Jakeman 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Ladybird { 17 | 18 | class EventLoopManagerGLib final : public Core::EventLoopManager { 19 | public: 20 | EventLoopManagerGLib(); 21 | virtual ~EventLoopManagerGLib() override; 22 | virtual NonnullOwnPtr make_implementation() override; 23 | 24 | virtual int register_timer(Core::Object&, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override; 25 | virtual bool unregister_timer(int timer_id) override; 26 | 27 | virtual void register_notifier(Core::Notifier&) override; 28 | virtual void unregister_notifier(Core::Notifier&) override; 29 | 30 | virtual void did_post_event() override; 31 | 32 | // FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them. 33 | virtual int register_signal(int, Function) override { return 0; } 34 | virtual void unregister_signal(int) override { } 35 | 36 | private: 37 | }; 38 | 39 | class EventLoopImplementationGLib final : public Core::EventLoopImplementation { 40 | public: 41 | static NonnullOwnPtr create() { return adopt_own(*new EventLoopImplementationGLib); } 42 | 43 | virtual ~EventLoopImplementationGLib() override; 44 | 45 | virtual int exec() override; 46 | virtual size_t pump(PumpMode) override; 47 | virtual void quit(int) override; 48 | virtual void wake() override; 49 | virtual void post_event(Core::Object& receiver, NonnullOwnPtr&&) override; 50 | 51 | // FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them. 52 | virtual void unquit() override { } 53 | virtual bool was_exit_requested() const override { return false; } 54 | virtual void notify_forked_and_in_child() override { } 55 | 56 | void set_main_loop() { dbgln("GLib Event Loop only supports being main!"); } 57 | 58 | private: 59 | friend class EventLoopManagerGLib; 60 | 61 | EventLoopImplementationGLib(); 62 | bool is_main_loop() const { return true; } 63 | 64 | GMainLoop *m_event_loop; 65 | int m_error_code; 66 | }; 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/EventLoopImplementationGtk.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023, Andreas Kling 3 | * Copyright (c) 2023, Matthew Jakeman 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include "EventLoopImplementationGtk.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Ladybird { 17 | 18 | struct ThreadData; 19 | static thread_local ThreadData* s_thread_data; 20 | 21 | struct ThreadData { 22 | static ThreadData& the() 23 | { 24 | if (!s_thread_data) { 25 | // FIXME: Don't leak this. 26 | s_thread_data = new ThreadData; 27 | } 28 | return *s_thread_data; 29 | } 30 | 31 | IDAllocator timer_id_allocator; 32 | HashMap> timers; 33 | HashMap> notifiers; 34 | HashMap> fd_channels; 35 | }; 36 | 37 | EventLoopImplementationGtk::EventLoopImplementationGtk() 38 | { 39 | // m_event_loop = g_main_loop_new(nullptr, FALSE); 40 | } 41 | 42 | EventLoopImplementationGtk::~EventLoopImplementationGtk() 43 | { 44 | // g_main_loop_unref(m_event_loop); 45 | } 46 | 47 | int EventLoopImplementationGtk::exec() 48 | { 49 | // g_main_loop_run(m_event_loop); 50 | GMainContext *context = g_main_context_default(); 51 | while (true) 52 | g_main_context_iteration(context, TRUE); 53 | return m_error_code; 54 | } 55 | 56 | size_t EventLoopImplementationGtk::pump(PumpMode mode) 57 | { 58 | VERIFY_NOT_REACHED(); 59 | auto result = Core::ThreadEventQueue::current().process(); 60 | if (mode == PumpMode::WaitForEvents) { 61 | // g_main_context_iteration(g_main_loop_get_context(m_event_loop), TRUE); 62 | } else { 63 | } 64 | result += Core::ThreadEventQueue::current().process(); 65 | return result; 66 | } 67 | 68 | void EventLoopImplementationGtk::quit(int code) 69 | { 70 | VERIFY_NOT_REACHED(); 71 | m_error_code = code; 72 | // g_main_loop_quit(m_event_loop); 73 | } 74 | 75 | void EventLoopImplementationGtk::wake() {} 76 | 77 | void EventLoopImplementationGtk::post_event(Core::Object& receiver, NonnullOwnPtr&& event) 78 | { 79 | // Can we have multithreaded event queues? 80 | m_thread_event_queue.post_event(receiver, move(event)); 81 | if (&m_thread_event_queue != &Core::ThreadEventQueue::current()) 82 | wake(); 83 | } 84 | 85 | static void glib_timer_fired(int timer_id, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible, Core::Object& object) 86 | { 87 | if (should_fire_when_not_visible == Core::TimerShouldFireWhenNotVisible::No) { 88 | if (!object.is_visible_for_timer_purposes()) 89 | return; 90 | } 91 | Core::TimerEvent event(timer_id); 92 | object.dispatch_event(event); 93 | } 94 | 95 | int EventLoopManagerGtk::register_timer(Core::Object& object, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible) 96 | { 97 | auto& thread_data = ThreadData::the(); 98 | 99 | auto timer_id = thread_data.timer_id_allocator.allocate(); 100 | auto weak_object = object.make_weak_ptr(); 101 | 102 | auto source = Glib::TimeoutSource::create(milliseconds); 103 | source->connect([timer_id, should_fire_when_not_visible, should_reload, weak_object = move(weak_object)]() -> bool { 104 | auto object = weak_object.strong_ref(); 105 | if (!object) 106 | return false; 107 | glib_timer_fired(timer_id, should_fire_when_not_visible, *object); 108 | return should_reload; 109 | }); 110 | source->attach(Glib::MainContext::get_default()); 111 | thread_data.timers.set(timer_id, move(source)); 112 | 113 | return timer_id; 114 | } 115 | 116 | bool EventLoopManagerGtk::unregister_timer(int timer_id) 117 | { 118 | auto& thread_data = ThreadData::the(); 119 | thread_data.timer_id_allocator.deallocate(timer_id); 120 | 121 | auto timer = thread_data.timers.get(timer_id); 122 | if (timer.has_value()) { 123 | timer->get()->destroy(); 124 | } 125 | 126 | return thread_data.timers.remove(timer_id); 127 | } 128 | 129 | void EventLoopManagerGtk::register_notifier(Core::Notifier& notifier) 130 | { 131 | Glib::IOCondition condition; 132 | switch (notifier.type()) { 133 | case Core::Notifier::Type::Read: 134 | condition = Glib::IOCondition::IO_IN; 135 | break; 136 | case Core::Notifier::Type::Write: 137 | condition = Glib::IOCondition::IO_OUT; 138 | break; 139 | default: 140 | TODO(); 141 | } 142 | 143 | auto fd = notifier.fd(); 144 | auto thread_data = ThreadData::the(); 145 | 146 | Glib::RefPtr channel; 147 | 148 | // Get existing channel 149 | auto maybe_channel = thread_data.fd_channels.get(fd); 150 | if (maybe_channel.has_value()) { 151 | channel = maybe_channel.value(); 152 | channel->reference(); 153 | } else { 154 | channel = Glib::IOChannel::create_from_fd(notifier.fd()); 155 | thread_data.fd_channels.set(fd, channel); 156 | 157 | // FIXME: Add callback to remove from hashmap 158 | // channel->add_destroy_notify_callback(nullptr, ...); 159 | } 160 | 161 | auto io_watch = channel->create_watch(condition); 162 | 163 | io_watch->connect([condition, ¬ifier](Glib::IOCondition cond) -> bool { 164 | if (cond == condition) { 165 | Core::NotifierActivationEvent event(notifier.fd()); 166 | notifier.dispatch_event(event); 167 | } 168 | return G_SOURCE_CONTINUE; 169 | }); 170 | io_watch->attach(Glib::MainContext::get_default()); 171 | 172 | // Store watch ID for later 173 | // ThreadData::the().notifiers.set(¬ifier, move(io_watch)); 174 | } 175 | 176 | void EventLoopManagerGtk::unregister_notifier(Core::Notifier& notifier) 177 | { 178 | auto thread_data = ThreadData::the(); 179 | 180 | auto watch = thread_data.notifiers.get(¬ifier); 181 | if (watch.has_value()) { 182 | watch->get()->destroy(); 183 | } 184 | 185 | auto channel = thread_data.fd_channels.get(notifier.fd()); 186 | if (channel.has_value()) { 187 | channel->get()->unreference(); 188 | } 189 | 190 | ThreadData::the().notifiers.remove(¬ifier); 191 | } 192 | 193 | void cb_process_events2() { 194 | Core::ThreadEventQueue::current().process(); 195 | } 196 | 197 | void EventLoopManagerGtk::did_post_event() 198 | { 199 | g_timeout_add_once(0, reinterpret_cast(cb_process_events2), nullptr); 200 | } 201 | 202 | EventLoopManagerGtk::EventLoopManagerGtk() 203 | { 204 | g_timeout_add_once(0, reinterpret_cast(cb_process_events2), nullptr); 205 | } 206 | 207 | EventLoopManagerGtk::~EventLoopManagerGtk() = default; 208 | 209 | NonnullOwnPtr EventLoopManagerGtk::make_implementation() 210 | { 211 | return adopt_own(*new EventLoopImplementationGtk); 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /src/EventLoopImplementationGtk.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023, Andreas Kling 3 | * Copyright (c) 2023, Matthew Jakeman 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Ladybird { 17 | 18 | class EventLoopManagerGtk final : public Core::EventLoopManager { 19 | public: 20 | EventLoopManagerGtk(); 21 | virtual ~EventLoopManagerGtk() override; 22 | virtual NonnullOwnPtr make_implementation() override; 23 | 24 | virtual int register_timer(Core::Object&, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override; 25 | virtual bool unregister_timer(int timer_id) override; 26 | 27 | virtual void register_notifier(Core::Notifier&) override; 28 | virtual void unregister_notifier(Core::Notifier&) override; 29 | 30 | virtual void did_post_event() override; 31 | 32 | // FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them. 33 | virtual int register_signal(int, Function) override { return 0; } 34 | virtual void unregister_signal(int) override { } 35 | 36 | private: 37 | }; 38 | 39 | class EventLoopImplementationGtk final : public Core::EventLoopImplementation { 40 | public: 41 | static NonnullOwnPtr create() { return adopt_own(*new EventLoopImplementationGtk); } 42 | 43 | virtual ~EventLoopImplementationGtk() override; 44 | 45 | virtual int exec() override; 46 | virtual size_t pump(PumpMode) override; 47 | virtual void quit(int) override; 48 | virtual void wake() override; 49 | virtual void post_event(Core::Object& receiver, NonnullOwnPtr&&) override; 50 | 51 | // FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them. 52 | virtual void unquit() override { } 53 | virtual bool was_exit_requested() const override { return false; } 54 | virtual void notify_forked_and_in_child() override { } 55 | 56 | void set_main_loop() { dbgln("GLib Event Loop only supports being main!"); } 57 | 58 | private: 59 | friend class EventLoopManagerGtk; 60 | 61 | EventLoopImplementationGtk(); 62 | bool is_main_loop() const { return true; } 63 | 64 | GMainLoop *m_event_loop; 65 | int m_error_code; 66 | }; 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/FontPluginPango.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * Copyright (c) 2023, Linus Groh 4 | * Copyright (c) 2023, Matthew Jakeman 5 | * 6 | * SPDX-License-Identifier: BSD-2-Clause 7 | */ 8 | 9 | #include "FontPluginPango.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | // #include 16 | // #include 17 | #include 18 | 19 | extern DeprecatedString s_serenity_resource_root; 20 | 21 | namespace Ladybird { 22 | 23 | FontPluginGTK::FontPluginGTK(bool is_layout_test_mode) 24 | : m_is_layout_test_mode(is_layout_test_mode) 25 | { 26 | // Load the default SerenityOS fonts... 27 | Gfx::FontDatabase::set_default_fonts_lookup_path(DeprecatedString::formatted("{}/res/fonts", s_serenity_resource_root)); 28 | 29 | // ...and also anything we can find in the system's font directories 30 | for (auto const& path : Core::StandardPaths::font_directories().release_value_but_fixme_should_propagate_errors()) 31 | Gfx::FontDatabase::the().load_all_fonts_from_path(path.to_deprecated_string()); 32 | 33 | Gfx::FontDatabase::set_default_font_query("Katica 10 400 0"); 34 | Gfx::FontDatabase::set_fixed_width_font_query("Csilla 10 400 0"); 35 | 36 | Gfx::Emoji::set_emoji_lookup_path(String::formatted("{}/res/emoji", s_serenity_resource_root).release_value_but_fixme_should_propagate_errors()); 37 | 38 | update_generic_fonts(); 39 | 40 | auto default_font_name = generic_font_name(Web::Platform::GenericFont::UiSansSerif); 41 | m_default_font = Gfx::FontDatabase::the().get(default_font_name, 12.0, 400, Gfx::FontWidth::Normal, 0); 42 | VERIFY(m_default_font); 43 | 44 | auto default_fixed_width_font_name = generic_font_name(Web::Platform::GenericFont::UiMonospace); 45 | m_default_fixed_width_font = Gfx::FontDatabase::the().get(default_fixed_width_font_name, 12.0, 400, Gfx::FontWidth::Normal, 0); 46 | VERIFY(m_default_fixed_width_font); 47 | 48 | // m_default_font = Gfx::FontDatabase::the().get("Arial", 12.0, 400, Gfx::FontWidth::Normal, 0); 49 | // VERIFY(m_default_font); 50 | // 51 | // m_default_fixed_width_font = Gfx::FontDatabase::the().get("Andale Mono", 12.0, 400, Gfx::FontWidth::Normal, 0); 52 | // VERIFY(m_default_fixed_width_font); 53 | } 54 | 55 | FontPluginGTK::~FontPluginGTK() = default; 56 | 57 | Gfx::Font& FontPluginGTK::default_font() 58 | { 59 | return *m_default_font; 60 | } 61 | 62 | Gfx::Font& FontPluginGTK::default_fixed_width_font() 63 | { 64 | return *m_default_fixed_width_font; 65 | } 66 | 67 | void FontPluginGTK::update_generic_fonts() 68 | { 69 | // How we choose which system font to use for each CSS font: 70 | // 1. Ask Qt via the QFont::StyleHint mechanism for the user's preferred font. 71 | // 2. Try loading that font through Gfx::FontDatabase 72 | // 3. If we don't support that font for whatever reason (e.g missing TrueType features in LibGfx)... 73 | // 1. Try a list of known-suitable fallback fonts with their names hard-coded below 74 | // 2. If that didn't work, fall back to Gfx::FontDatabase::default_font() (or default_fixed_width_font()) 75 | 76 | // This is rather weird, but it's how things work right now, as we can only draw with fonts loaded by LibGfx. 77 | 78 | m_generic_font_names.resize(static_cast(Web::Platform::GenericFont::__Count)); 79 | 80 | auto update_mapping = [&](Web::Platform::GenericFont generic_font, ReadonlySpan fallbacks) { 81 | if (m_is_layout_test_mode) { 82 | m_generic_font_names[static_cast(generic_font)] = "SerenitySans"; 83 | return; 84 | } 85 | 86 | Pango::FontDescription font_desc; 87 | 88 | if (generic_font == Web::Platform::GenericFont::Monospace) 89 | font_desc.set_family("monospace"); 90 | else if (generic_font == Web::Platform::GenericFont::Fantasy) 91 | font_desc.set_family("fantasy"); 92 | else if (generic_font == Web::Platform::GenericFont::Cursive) 93 | font_desc.set_family("cursive"); 94 | 95 | auto pango_font_family = font_desc.get_family(); 96 | 97 | auto gfx_font = Gfx::FontDatabase::the().get(pango_font_family.c_str(), 16, 400, Gfx::FontWidth::Normal, 0, Gfx::Font::AllowInexactSizeMatch::Yes); 98 | if (!gfx_font) { 99 | for (auto& fallback : fallbacks) { 100 | gfx_font = Gfx::FontDatabase::the().get(fallback, 16, 400, Gfx::FontWidth::Normal, 0, Gfx::Font::AllowInexactSizeMatch::Yes); 101 | if (gfx_font) 102 | break; 103 | } 104 | } 105 | 106 | if (!gfx_font) { 107 | if (generic_font == Web::Platform::GenericFont::Monospace || generic_font == Web::Platform::GenericFont::UiMonospace) 108 | gfx_font = Gfx::FontDatabase::default_fixed_width_font(); 109 | else 110 | gfx_font = Gfx::FontDatabase::default_font(); 111 | } 112 | 113 | m_generic_font_names[static_cast(generic_font)] = gfx_font->family(); 114 | }; 115 | 116 | // Fallback fonts to look for if Gfx::Font can't load the font suggested by Qt. 117 | // The lists are basically arbitrary, taken from https://www.w3.org/Style/Examples/007/fonts.en.html 118 | Vector cursive_fallbacks { "Comic Sans MS", "Comic Sans", "Apple Chancery", "Bradley Hand", "Brush Script MT", "Snell Roundhand", "URW Chancery L" }; 119 | Vector fantasy_fallbacks { "Impact", "Luminari", "Chalkduster", "Jazz LET", "Blippo", "Stencil Std", "Marker Felt", "Trattatello" }; 120 | Vector monospace_fallbacks { "Andale Mono", "Courier New", "Courier", "FreeMono", "OCR A Std", "DejaVu Sans Mono", "Liberation Mono", "Csilla" }; 121 | Vector sans_serif_fallbacks { "Arial", "Helvetica", "Verdana", "Trebuchet MS", "Gill Sans", "Noto Sans", "Avantgarde", "Optima", "Arial Narrow", "Liberation Sans", "Katica" }; 122 | Vector serif_fallbacks { "Times", "Times New Roman", "Didot", "Georgia", "Palatino", "Bookman", "New Century Schoolbook", "American Typewriter", "Liberation Serif", "Roman" }; 123 | 124 | update_mapping(Web::Platform::GenericFont::Cursive, cursive_fallbacks); 125 | update_mapping(Web::Platform::GenericFont::Fantasy, fantasy_fallbacks); 126 | update_mapping(Web::Platform::GenericFont::Monospace, monospace_fallbacks); 127 | update_mapping(Web::Platform::GenericFont::SansSerif, sans_serif_fallbacks); 128 | update_mapping(Web::Platform::GenericFont::Serif, serif_fallbacks); 129 | update_mapping(Web::Platform::GenericFont::UiMonospace, monospace_fallbacks); 130 | update_mapping(Web::Platform::GenericFont::UiRounded, sans_serif_fallbacks); 131 | update_mapping(Web::Platform::GenericFont::UiSansSerif, sans_serif_fallbacks); 132 | update_mapping(Web::Platform::GenericFont::UiSerif, serif_fallbacks); 133 | } 134 | 135 | DeprecatedString FontPluginGTK::generic_font_name(Web::Platform::GenericFont generic_font) 136 | { 137 | return m_generic_font_names[static_cast(generic_font)]; 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/FontPluginPango.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * Copyright (c) 2023, Matthew Jakeman 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace Ladybird { 15 | 16 | class FontPluginGTK final : public Web::Platform::FontPlugin { 17 | public: 18 | FontPluginGTK(bool is_layout_test_mode); 19 | virtual ~FontPluginGTK(); 20 | 21 | virtual Gfx::Font& default_font() override; 22 | virtual Gfx::Font& default_fixed_width_font() override; 23 | virtual DeprecatedString generic_font_name(Web::Platform::GenericFont) override; 24 | 25 | void update_generic_fonts(); 26 | 27 | private: 28 | Vector m_generic_font_names; 29 | RefPtr m_default_font; 30 | RefPtr m_default_fixed_width_font; 31 | bool m_is_layout_test_mode { false }; 32 | }; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/HelperProcess.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Andrew Kaster 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "HelperProcess.h" 8 | #include "Utilities.h" 9 | #include 10 | #include 11 | 12 | ErrorOr spawn_helper_process(StringView process_name, ReadonlySpan arguments, Core::System::SearchInPath search_in_path, Optional> environment) 13 | { 14 | auto paths = TRY(get_paths_for_helper_process(process_name)); 15 | VERIFY(!paths.is_empty()); 16 | ErrorOr result; 17 | for (auto const& path : paths) { 18 | result = Core::System::exec(path, arguments, search_in_path, environment); 19 | if (!result.is_error()) 20 | break; 21 | } 22 | 23 | return result; 24 | } 25 | 26 | ErrorOr> get_paths_for_helper_process(StringView process_name) 27 | { 28 | auto application_path = TRY(ak_string_from_cstring(g_get_current_dir())); 29 | Vector paths; 30 | 31 | TRY(paths.try_append(TRY(String::formatted("./{}/{}", process_name, process_name)))); 32 | TRY(paths.try_append(TRY(String::formatted("{}/{}/{}", application_path, process_name, process_name)))); 33 | TRY(paths.try_append(TRY(String::formatted("{}/{}", application_path, process_name)))); 34 | TRY(paths.try_append(TRY(String::formatted("./{}", process_name)))); 35 | // NOTE: Add platform-specific paths here 36 | return paths; 37 | } 38 | -------------------------------------------------------------------------------- /src/HelperProcess.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Andrew Kaster 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | ErrorOr spawn_helper_process(StringView process_name, ReadonlySpan arguments, Core::System::SearchInPath, Optional> environment = {}); 16 | ErrorOr> get_paths_for_helper_process(StringView process_name); 17 | -------------------------------------------------------------------------------- /src/Icons/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 33 | 35 | 39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Icons/forward.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 33 | 35 | 39 | 43 | 47 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Icons/ladybird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjakeman/libweb-gtk/c60082935a5af1ffca405b15473d82ca0c8863cb/src/Icons/ladybird.png -------------------------------------------------------------------------------- /src/Icons/reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 33 | 35 | 39 | 43 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/ImageCodecPluginLadybird.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Dex♪ 3 | * Copyright (c) 2022, Andreas Kling 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include "ImageCodecPluginLadybird.h" 9 | #include 10 | #include 11 | 12 | namespace Ladybird { 13 | 14 | ImageCodecPluginLadybird::~ImageCodecPluginLadybird() = default; 15 | 16 | Optional ImageCodecPluginLadybird::decode_image(ReadonlyBytes data) 17 | { 18 | auto decoder = Gfx::ImageDecoder::try_create_for_raw_bytes(data); 19 | 20 | if (!decoder || !decoder->frame_count()) { 21 | return {}; 22 | } 23 | 24 | Vector frames; 25 | for (size_t i = 0; i < decoder->frame_count(); ++i) { 26 | auto frame_or_error = decoder->frame(i); 27 | if (frame_or_error.is_error()) 28 | return {}; 29 | auto frame = frame_or_error.release_value(); 30 | frames.append({ move(frame.image), static_cast(frame.duration) }); 31 | } 32 | 33 | return Web::Platform::DecodedImage { 34 | decoder->is_animated(), 35 | static_cast(decoder->loop_count()), 36 | move(frames), 37 | }; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/ImageCodecPluginLadybird.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Dex♪ 3 | * Copyright (c) 2022, Andreas Kling 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace Ladybird { 13 | 14 | class ImageCodecPluginLadybird final : public Web::Platform::ImageCodecPlugin { 15 | public: 16 | ImageCodecPluginLadybird() = default; 17 | virtual ~ImageCodecPluginLadybird() override; 18 | 19 | virtual Optional decode_image(ReadonlyBytes data) override; 20 | }; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/InspectorWidget.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, MacDue 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "InspectorWidget.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | namespace Ladybird { 22 | 23 | InspectorWidget::InspectorWidget() 24 | { 25 | setLayout(new QVBoxLayout); 26 | auto splitter = new QSplitter(this); 27 | layout()->addWidget(splitter); 28 | splitter->setOrientation(Qt::Vertical); 29 | 30 | auto add_tab = [&](auto* tab_widget, auto* widget, auto name) { 31 | auto container = new QWidget; 32 | container->setLayout(new QVBoxLayout); 33 | container->layout()->addWidget(widget); 34 | tab_widget->addTab(container, name); 35 | }; 36 | 37 | auto top_tap_widget = new QTabWidget; 38 | splitter->addWidget(top_tap_widget); 39 | 40 | m_dom_tree_view = new QTreeView; 41 | m_dom_tree_view->setHeaderHidden(true); 42 | m_dom_tree_view->setModel(&m_dom_model); 43 | QObject::connect(m_dom_tree_view->selectionModel(), &QItemSelectionModel::selectionChanged, 44 | [this](QItemSelection const& selected, QItemSelection const&) { 45 | auto indexes = selected.indexes(); 46 | if (indexes.size()) { 47 | auto index = m_dom_model.to_gui(indexes.first()); 48 | set_selection(index); 49 | } 50 | }); 51 | add_tab(top_tap_widget, m_dom_tree_view, "DOM"); 52 | 53 | auto accessibility_tree_view = new QTreeView; 54 | accessibility_tree_view->setHeaderHidden(true); 55 | accessibility_tree_view->setModel(&m_accessibility_model); 56 | add_tab(top_tap_widget, accessibility_tree_view, "Accessibility"); 57 | 58 | auto add_table_tab = [&](auto* tab_widget, auto& model, auto name) { 59 | auto table_view = new QTableView; 60 | table_view->setModel(&model); 61 | table_view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); 62 | table_view->verticalHeader()->setVisible(false); 63 | table_view->horizontalHeader()->setVisible(false); 64 | add_tab(tab_widget, table_view, name); 65 | }; 66 | 67 | auto node_tabs = new QTabWidget; 68 | add_table_tab(node_tabs, m_computed_style_model, "Computed"); 69 | add_table_tab(node_tabs, m_resolved_style_model, "Resolved"); 70 | add_table_tab(node_tabs, m_custom_properties_model, "Variables"); 71 | splitter->addWidget(node_tabs); 72 | } 73 | 74 | void InspectorWidget::set_dom_json(StringView dom_json) 75 | { 76 | m_dom_model.set_underlying_model(WebView::DOMTreeModel::create(dom_json)); 77 | m_dom_loaded = true; 78 | if (m_pending_selection.has_value()) 79 | set_selection(m_pending_selection.release_value()); 80 | else 81 | select_default_node(); 82 | } 83 | 84 | void InspectorWidget::set_accessibility_json(StringView accessibility_json) 85 | { 86 | m_accessibility_model.set_underlying_model(WebView::AccessibilityTreeModel::create(accessibility_json)); 87 | } 88 | 89 | void InspectorWidget::clear_dom_json() 90 | { 91 | m_dom_model.set_underlying_model(nullptr); 92 | // The accessibility tree is pretty much another form of the DOM tree, so should be cleared at the time time. 93 | m_accessibility_model.set_underlying_model(nullptr); 94 | clear_style_json(); 95 | clear_selection(); 96 | m_dom_loaded = false; 97 | } 98 | 99 | void InspectorWidget::load_style_json(StringView computed_style_json, StringView resolved_style_json, StringView custom_properties_json) 100 | { 101 | m_computed_style_model.set_underlying_model(WebView::StylePropertiesModel::create(computed_style_json)); 102 | m_resolved_style_model.set_underlying_model(WebView::StylePropertiesModel::create(resolved_style_json)); 103 | m_custom_properties_model.set_underlying_model(WebView::StylePropertiesModel::create(custom_properties_json)); 104 | } 105 | 106 | void InspectorWidget::clear_style_json() 107 | { 108 | m_computed_style_model.set_underlying_model(nullptr); 109 | m_resolved_style_model.set_underlying_model(nullptr); 110 | m_custom_properties_model.set_underlying_model(nullptr); 111 | clear_selection(); 112 | } 113 | 114 | void InspectorWidget::closeEvent(QCloseEvent* event) 115 | { 116 | event->accept(); 117 | if (on_close) 118 | on_close(); 119 | clear_selection(); 120 | } 121 | 122 | void InspectorWidget::clear_selection() 123 | { 124 | m_selection = {}; 125 | m_dom_tree_view->clearSelection(); 126 | } 127 | 128 | void InspectorWidget::set_selection(Selection selection) 129 | { 130 | if (!m_dom_loaded) { 131 | m_pending_selection = selection; 132 | return; 133 | } 134 | 135 | auto* model = verify_cast(m_dom_model.underlying_model().ptr()); 136 | auto index = model->index_for_node(selection.dom_node_id, selection.pseudo_element); 137 | auto qt_index = m_dom_model.to_qt(index); 138 | 139 | if (!qt_index.isValid()) { 140 | dbgln("Failed to set DOM inspector selection! Could not find valid model index for node: {}", selection.dom_node_id); 141 | return; 142 | } 143 | 144 | m_dom_tree_view->scrollTo(qt_index); 145 | m_dom_tree_view->setCurrentIndex(qt_index); 146 | } 147 | 148 | void InspectorWidget::select_default_node() 149 | { 150 | clear_style_json(); 151 | m_dom_tree_view->collapseAll(); 152 | m_dom_tree_view->setCurrentIndex({}); 153 | } 154 | 155 | void InspectorWidget::set_selection(GUI::ModelIndex index) 156 | { 157 | if (!index.is_valid()) 158 | return; 159 | 160 | auto* json = static_cast(index.internal_data()); 161 | VERIFY(json); 162 | 163 | Selection selection {}; 164 | if (json->has_u32("pseudo-element"sv)) { 165 | selection.dom_node_id = json->get_i32("parent-id"sv).value(); 166 | selection.pseudo_element = static_cast(json->get_u32("pseudo-element"sv).value()); 167 | } else { 168 | selection.dom_node_id = json->get_i32("id"sv).value(); 169 | } 170 | 171 | if (selection == m_selection) 172 | return; 173 | m_selection = selection; 174 | 175 | VERIFY(on_dom_node_inspected); 176 | auto maybe_inspected_node_properties = on_dom_node_inspected(m_selection.dom_node_id, m_selection.pseudo_element); 177 | if (!maybe_inspected_node_properties.is_error()) { 178 | auto properties = maybe_inspected_node_properties.release_value(); 179 | load_style_json(properties.computed_style_json, properties.resolved_style_json, properties.custom_properties_json); 180 | } else { 181 | clear_style_json(); 182 | } 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /src/InspectorWidget.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, MacDue 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "ModelTranslator.h" 10 | #include "WebContentView.h" 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | class QTreeView; 17 | class QTableView; 18 | 19 | namespace Ladybird { 20 | 21 | class InspectorWidget final : public QWidget { 22 | Q_OBJECT 23 | public: 24 | InspectorWidget(); 25 | virtual ~InspectorWidget() = default; 26 | 27 | struct Selection { 28 | i32 dom_node_id { 0 }; 29 | Optional pseudo_element {}; 30 | bool operator==(Selection const& other) const = default; 31 | }; 32 | 33 | bool dom_loaded() const { return m_dom_loaded; } 34 | 35 | void set_selection(Selection); 36 | void clear_selection(); 37 | 38 | void select_default_node(); 39 | 40 | void clear_dom_json(); 41 | void set_dom_json(StringView dom_json); 42 | 43 | void set_accessibility_json(StringView accessibility_json); 44 | 45 | void load_style_json(StringView computed_style_json, StringView resolved_style_json, StringView custom_properties_json); 46 | void clear_style_json(); 47 | 48 | Function(i32, Optional)> on_dom_node_inspected; 49 | Function on_close; 50 | 51 | private: 52 | void set_selection(GUI::ModelIndex); 53 | void closeEvent(QCloseEvent*) override; 54 | 55 | Selection m_selection; 56 | 57 | ModelTranslator m_dom_model {}; 58 | ModelTranslator m_accessibility_model {}; 59 | ModelTranslator m_computed_style_model {}; 60 | ModelTranslator m_resolved_style_model {}; 61 | ModelTranslator m_custom_properties_model {}; 62 | 63 | QTreeView* m_dom_tree_view { nullptr }; 64 | 65 | bool m_dom_loaded { false }; 66 | Optional m_pending_selection {}; 67 | }; 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/LocationEdit.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Cameron Youell 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "LocationEdit.h" 8 | #include "Utilities.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | LocationEdit::LocationEdit(QWidget* parent) 16 | : QLineEdit(parent) 17 | { 18 | setPlaceholderText("Enter web address"); 19 | connect(this, &QLineEdit::returnPressed, this, [&] { 20 | clearFocus(); 21 | }); 22 | 23 | connect(this, &QLineEdit::textChanged, this, [&] { 24 | highlight_location(); 25 | }); 26 | } 27 | 28 | void LocationEdit::focusInEvent(QFocusEvent* event) 29 | { 30 | QLineEdit::focusInEvent(event); 31 | highlight_location(); 32 | QTimer::singleShot(0, this, &QLineEdit::selectAll); 33 | } 34 | 35 | void LocationEdit::focusOutEvent(QFocusEvent* event) 36 | { 37 | QLineEdit::focusOutEvent(event); 38 | highlight_location(); 39 | } 40 | 41 | void LocationEdit::highlight_location() 42 | { 43 | auto url = AK::URL::create_with_url_or_path(ak_deprecated_string_from_qstring(text())); 44 | 45 | auto darkened_text_color = QPalette().color(QPalette::Text); 46 | darkened_text_color.setAlpha(127); 47 | 48 | QList attributes; 49 | if (url.is_valid() && !hasFocus()) { 50 | if (url.scheme() == "http" || url.scheme() == "https" || url.scheme() == "gemini") { 51 | int host_start = (url.scheme().length() + 3) - cursorPosition(); 52 | auto host_length = url.host().length(); 53 | 54 | // FIXME: Maybe add a generator to use https://publicsuffix.org/list/public_suffix_list.dat 55 | // for now just highlight the whole host 56 | 57 | QTextCharFormat defaultFormat; 58 | defaultFormat.setForeground(darkened_text_color); 59 | attributes.append({ 60 | QInputMethodEvent::TextFormat, 61 | -cursorPosition(), 62 | static_cast(text().length()), 63 | defaultFormat, 64 | }); 65 | 66 | QTextCharFormat hostFormat; 67 | hostFormat.setForeground(QPalette().color(QPalette::Text)); 68 | attributes.append({ 69 | QInputMethodEvent::TextFormat, 70 | host_start, 71 | static_cast(host_length), 72 | hostFormat, 73 | }); 74 | } else if (url.scheme() == "file") { 75 | QTextCharFormat schemeFormat; 76 | schemeFormat.setForeground(darkened_text_color); 77 | attributes.append({ 78 | QInputMethodEvent::TextFormat, 79 | -cursorPosition(), 80 | static_cast(url.scheme().length() + 3), 81 | schemeFormat, 82 | }); 83 | } 84 | } 85 | 86 | QInputMethodEvent event(QString(), attributes); 87 | QCoreApplication::sendEvent(this, &event); 88 | } 89 | -------------------------------------------------------------------------------- /src/LocationEdit.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Cameron Youell 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | class LocationEdit final : public QLineEdit { 12 | Q_OBJECT 13 | public: 14 | explicit LocationEdit(QWidget*); 15 | 16 | private: 17 | virtual void focusInEvent(QFocusEvent* event) override; 18 | virtual void focusOutEvent(QFocusEvent* event) override; 19 | 20 | void highlight_location(); 21 | }; 22 | -------------------------------------------------------------------------------- /src/ModelTranslator.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "ModelTranslator.h" 8 | #include "Utilities.h" 9 | #include 10 | 11 | namespace Ladybird { 12 | 13 | ModelTranslator::~ModelTranslator() = default; 14 | 15 | int ModelTranslator::columnCount(QModelIndex const& parent) const 16 | { 17 | if (!m_model) 18 | return 0; 19 | return m_model->column_count(to_gui(parent)); 20 | } 21 | 22 | int ModelTranslator::rowCount(QModelIndex const& parent) const 23 | { 24 | if (!m_model) 25 | return 0; 26 | return m_model->row_count(to_gui(parent)); 27 | } 28 | 29 | static QVariant convert_variant(GUI::Variant const& value) 30 | { 31 | if (value.is_string()) 32 | return qstring_from_ak_deprecated_string(value.as_string()); 33 | if (value.is_icon()) { 34 | auto const& gui_icon = value.as_icon(); 35 | auto bitmap = gui_icon.bitmap_for_size(16); 36 | VERIFY(bitmap); 37 | auto qt_image = QImage(bitmap->scanline_u8(0), 16, 16, QImage::Format_ARGB32); 38 | QIcon qt_icon; 39 | qt_icon.addPixmap(QPixmap::fromImage(qt_image.convertToFormat(QImage::Format::Format_ARGB32_Premultiplied))); 40 | return qt_icon; 41 | } 42 | return {}; 43 | } 44 | 45 | QVariant ModelTranslator::data(QModelIndex const& index, int role) const 46 | { 47 | VERIFY(m_model); 48 | switch (role) { 49 | case Qt::DisplayRole: 50 | return convert_variant(m_model->data(to_gui(index), GUI::ModelRole::Display)); 51 | case Qt::DecorationRole: 52 | return convert_variant(m_model->data(to_gui(index), GUI::ModelRole::Icon)); 53 | default: 54 | return {}; 55 | } 56 | } 57 | 58 | QModelIndex ModelTranslator::index(int row, int column, QModelIndex const& parent) const 59 | { 60 | VERIFY(m_model); 61 | return to_qt(m_model->index(row, column, to_gui(parent))); 62 | } 63 | 64 | QModelIndex ModelTranslator::parent(QModelIndex const& index) const 65 | { 66 | VERIFY(m_model); 67 | return to_qt(m_model->parent_index(to_gui(index))); 68 | } 69 | 70 | QModelIndex ModelTranslator::to_qt(GUI::ModelIndex const& index) const 71 | { 72 | if (!index.is_valid()) 73 | return {}; 74 | return createIndex(index.row(), index.column(), index.internal_data()); 75 | } 76 | 77 | GUI::ModelIndex ModelTranslator::to_gui(QModelIndex const& index) const 78 | { 79 | VERIFY(m_model); 80 | if (!index.isValid()) 81 | return {}; 82 | return m_model->unsafe_create_index(index.row(), index.column(), index.internalPointer()); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/ModelTranslator.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | namespace Ladybird { 13 | 14 | class ModelTranslator final : public QAbstractItemModel { 15 | Q_OBJECT 16 | public: 17 | virtual ~ModelTranslator() override; 18 | 19 | void set_underlying_model(RefPtr model) 20 | { 21 | beginResetModel(); 22 | m_model = model; 23 | endResetModel(); 24 | } 25 | 26 | RefPtr underlying_model() 27 | { 28 | return m_model; 29 | } 30 | 31 | virtual int columnCount(QModelIndex const& parent) const override; 32 | virtual int rowCount(QModelIndex const& parent) const override; 33 | virtual QVariant data(QModelIndex const&, int role) const override; 34 | virtual QModelIndex index(int row, int column, QModelIndex const& parent) const override; 35 | virtual QModelIndex parent(QModelIndex const& index) const override; 36 | 37 | QModelIndex to_qt(GUI::ModelIndex const&) const; 38 | GUI::ModelIndex to_gui(QModelIndex const&) const; 39 | 40 | private: 41 | RefPtr m_model; 42 | }; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Ladybird 2 | 3 | Ladybird is a web browser built on the [LibWeb](https://github.com/SerenityOS/serenity/tree/master/Userland/Libraries/LibWeb) and [LibJS](https://github.com/SerenityOS/serenity/tree/master/Userland/Libraries/LibJS) engines from [SerenityOS](https://github.com/SerenityOS/serenity) with a cross-platform GUI in Qt. 4 | 5 | For more information about Ladybird, see [this blog post](https://awesomekling.github.io/Ladybird-a-new-cross-platform-browser-project/). 6 | 7 | See [build instructions](../Documentation/BuildInstructionsLadybird.md). 8 | -------------------------------------------------------------------------------- /src/RequestManagerSoup.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Matthew Jakeman 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "RequestManagerSoup.h" 8 | #include "Utilities.h" 9 | #include 10 | 11 | RequestManagerSoup::RequestManagerSoup() 12 | { 13 | m_session = soup_session_new(); 14 | } 15 | 16 | void RequestManagerSoup::reply_finished(SoupSession* session, GAsyncResult* result, gpointer user_data) 17 | { 18 | auto *self = static_cast(user_data); 19 | SoupMessage *reply = soup_session_get_async_result_message(session, result); 20 | auto request = self->m_pending.get(reply).value(); 21 | self->m_pending.remove(reply); 22 | request->did_finish(session, reply, result); 23 | } 24 | 25 | RefPtr RequestManagerSoup::start_request(DeprecatedString const& method, AK::URL const& url, HashMap const& request_headers, ReadonlyBytes request_body, Core::ProxyData const& proxy) 26 | { 27 | if (!url.scheme().is_one_of_ignoring_ascii_case("http"sv, "https"sv)) { 28 | return nullptr; 29 | } 30 | auto request_or_error = create_request(m_session, method, url, request_headers, request_body, proxy); 31 | if (request_or_error.is_error()) { 32 | return nullptr; 33 | } 34 | auto request = request_or_error.release_value(); 35 | m_pending.set(request->reply(), *request); 36 | return request; 37 | } 38 | 39 | ErrorOr> RequestManagerSoup::create_request(SoupSession *session, DeprecatedString const& method, AK::URL const& url, HashMap const& request_headers, ReadonlyBytes request_body, Core::ProxyData const&) 40 | { 41 | SoupMessageHeaders *soup_request_headers; 42 | SoupMessage *msg; 43 | 44 | /* Initialize URL and headers */ 45 | char *c_url = owned_cstring_from_ak_string(url.to_string().value()); 46 | 47 | if (method.equals_ignoring_ascii_case("head"sv)) { 48 | msg = soup_message_new (SOUP_METHOD_HEAD, c_url); 49 | } else if (method.equals_ignoring_ascii_case("get"sv)) { 50 | msg = soup_message_new (SOUP_METHOD_GET, c_url); 51 | } else if (method.equals_ignoring_ascii_case("post"sv)) { 52 | msg = soup_message_new (SOUP_METHOD_POST, c_url); 53 | 54 | GBytes *request_bytes = g_bytes_new_static ((char const*)request_body.data(), request_body.size()); 55 | soup_message_set_request_body_from_bytes(msg, "application/octet-stream", request_bytes); 56 | g_bytes_unref (request_bytes); 57 | } else if (method.equals_ignoring_ascii_case("put"sv)) { 58 | msg = soup_message_new (SOUP_METHOD_PUT, c_url); 59 | 60 | GBytes *request_bytes = g_bytes_new_static ((char const*)request_body.data(), request_body.size()); 61 | soup_message_set_request_body_from_bytes(msg, "application/octet-stream", request_bytes); 62 | g_bytes_unref (request_bytes); 63 | } else if (method.equals_ignoring_ascii_case("delete"sv)) { 64 | msg = soup_message_new (SOUP_METHOD_DELETE, c_url); 65 | } else { 66 | // Custom e.g. for HTTP OPTIONS 67 | // Do we need this? 68 | msg = soup_message_new (method.characters(), c_url); 69 | GBytes *request_bytes = g_bytes_new_static ((char const*)request_body.data(), request_body.size()); 70 | soup_message_set_request_body_from_bytes(msg, "application/octet-stream", request_bytes); 71 | g_bytes_unref (request_bytes); 72 | } 73 | 74 | g_free(c_url); 75 | 76 | soup_request_headers = soup_message_get_request_headers(msg); 77 | 78 | for (auto& it : request_headers) { 79 | if (g_ascii_strcasecmp(it.key.characters(), "Accept-Encoding") == 0) 80 | continue; 81 | soup_message_headers_append(soup_request_headers, it.key.characters(), it.value.characters()); 82 | } 83 | 84 | /* NOTE: We explicitly disable HTTP2 as it's significantly slower (up to 5x, possibly more) */ 85 | soup_message_set_force_http1 (msg, true); 86 | 87 | soup_session_send_and_read_async ( 88 | session, 89 | msg, 90 | G_PRIORITY_DEFAULT, 91 | nullptr, 92 | reinterpret_cast(reply_finished), 93 | this); 94 | 95 | return adopt_ref (*new Request(msg)); 96 | } 97 | 98 | RequestManagerSoup::Request::Request(SoupMessage *reply) 99 | : m_reply(reply) 100 | { 101 | } 102 | 103 | RequestManagerSoup::Request::~Request() = default; 104 | 105 | void RequestManagerSoup::Request::did_finish(SoupSession *session, SoupMessage *reply, GAsyncResult *result) 106 | { 107 | GError *error = nullptr; 108 | GBytes *buffer = soup_session_send_and_read_finish(session, result, &error); 109 | 110 | if (error) { 111 | dbgln("Request Error: {}", error->message); 112 | g_error_free(error); 113 | return; 114 | } 115 | 116 | auto http_status_code = soup_message_get_status(reply); 117 | auto http_response_headers = soup_message_get_response_headers(reply); 118 | HashMap response_headers; 119 | Vector set_cookie_headers; 120 | 121 | SoupMessageHeadersIter iter; 122 | const char *c_name, *c_value; 123 | 124 | soup_message_headers_iter_init(&iter, http_response_headers); 125 | while (soup_message_headers_iter_next(&iter, &c_name, &c_value)) 126 | { 127 | auto name = DeprecatedString(c_name); 128 | auto value = DeprecatedString(c_value); 129 | if (name.equals_ignoring_ascii_case("set-cookie"sv)) { 130 | set_cookie_headers.append(value); 131 | } else { 132 | response_headers.set(name, value); 133 | } 134 | } 135 | 136 | if (!set_cookie_headers.is_empty()) { 137 | response_headers.set("set-cookie"sv, JsonArray { set_cookie_headers }.to_deprecated_string()); 138 | } 139 | bool success = http_status_code != 0; 140 | gsize buffer_length; 141 | auto buffer_data = g_bytes_get_data(buffer, &buffer_length); 142 | on_buffered_request_finish(success, buffer_length, response_headers, http_status_code, ReadonlyBytes { buffer_data, (size_t)buffer_length }); 143 | g_bytes_unref(buffer); 144 | } 145 | -------------------------------------------------------------------------------- /src/RequestManagerSoup.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * Copyright (c) 2023, Matthew Jakeman 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | class RequestManagerSoup 15 | : Glib::Object 16 | , public Web::ResourceLoaderConnector { 17 | public: 18 | static NonnullRefPtr create() 19 | { 20 | return adopt_ref(*new RequestManagerSoup()); 21 | } 22 | 23 | virtual ~RequestManagerSoup() override { } 24 | 25 | virtual void prefetch_dns(AK::URL const&) override { } 26 | virtual void preconnect(AK::URL const&) override { } 27 | 28 | virtual RefPtr start_request(DeprecatedString const& method, AK::URL const&, HashMap const& request_headers, ReadonlyBytes request_body, Core::ProxyData const&) override; 29 | 30 | private: 31 | RequestManagerSoup(); 32 | 33 | class Request 34 | : public Web::ResourceLoaderConnectorRequest { 35 | friend RequestManagerSoup; 36 | public: 37 | virtual ~Request() override; 38 | 39 | virtual void set_should_buffer_all_input(bool) override { } 40 | virtual bool stop() override { return false; } 41 | virtual void stream_into(Stream&) override { } 42 | 43 | void did_finish(SoupSession *session, SoupMessage *reply, GAsyncResult *result); 44 | 45 | SoupMessage *reply() { return m_reply; } 46 | 47 | private: 48 | explicit Request(SoupMessage *message); 49 | 50 | SoupMessage *m_reply; 51 | }; 52 | 53 | ErrorOr> create_request(SoupSession *session, DeprecatedString const& method, AK::URL const& url, HashMap const& request_headers, ReadonlyBytes request_body, Core::ProxyData const&); 54 | static void reply_finished(SoupSession* session, GAsyncResult* result, gpointer); 55 | 56 | HashMap> m_pending; 57 | SoupSession* m_session; 58 | }; 59 | -------------------------------------------------------------------------------- /src/SQLServer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SQL_SERVER_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Services/SQLServer) 2 | 3 | set(SQL_SERVER_SOURCES 4 | ${SQL_SERVER_SOURCE_DIR}/ConnectionFromClient.cpp 5 | ${SQL_SERVER_SOURCE_DIR}/DatabaseConnection.cpp 6 | ${SQL_SERVER_SOURCE_DIR}/SQLStatement.cpp 7 | main.cpp 8 | ) 9 | 10 | add_executable(SQLServer ${SQL_SERVER_SOURCES}) 11 | 12 | target_include_directories(SQLServer PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/) 13 | target_include_directories(SQLServer PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..) 14 | target_link_libraries(SQLServer PRIVATE ${GTK4_LIBRARIES} LibCore LibFileSystem LibIPC LibSQL LibMain) 15 | -------------------------------------------------------------------------------- /src/SQLServer/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Tim Flynn 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | ErrorOr serenity_main(Main::Arguments arguments) 17 | { 18 | DeprecatedString pid_file; 19 | 20 | Core::ArgsParser args_parser; 21 | args_parser.add_option(pid_file, "Path to the PID file for the SQLServer singleton process", "pid-file", 'p', "pid_file"); 22 | args_parser.parse(arguments); 23 | 24 | VERIFY(!pid_file.is_empty()); 25 | 26 | auto database_path = DeprecatedString::formatted("{}/browser", Core::StandardPaths::data_directory()); 27 | TRY(Core::Directory::create(database_path, Core::Directory::CreateDirectories::Yes)); 28 | 29 | Core::EventLoop loop; 30 | 31 | auto server = TRY(IPC::MultiServer::try_create()); 32 | u64 connection_count { 0 }; 33 | 34 | server->on_new_client = [&](auto& client) { 35 | client.set_database_path(database_path); 36 | ++connection_count; 37 | 38 | client.on_disconnect = [&]() { 39 | if (--connection_count == 0) { 40 | MUST(Core::System::unlink(pid_file)); 41 | loop.quit(0); 42 | } 43 | }; 44 | }; 45 | 46 | return loop.exec(); 47 | } 48 | -------------------------------------------------------------------------------- /src/Settings.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Filiph Sandström 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "Settings.h" 8 | #include "Utilities.h" 9 | #include 10 | #include 11 | 12 | namespace Browser { 13 | 14 | static QString rebase_default_url_on_serenity_resource_root(StringView default_url) 15 | { 16 | URL url { default_url }; 17 | Vector paths; 18 | 19 | for (auto segment : s_serenity_resource_root.split('/')) 20 | paths.append(move(segment)); 21 | 22 | for (size_t i = 0; i < url.path_segment_count(); ++i) 23 | paths.append(url.path_segment_at_index(i)); 24 | 25 | url.set_paths(move(paths)); 26 | 27 | return qstring_from_ak_deprecated_string(url.to_deprecated_string()); 28 | } 29 | 30 | Settings::Settings() 31 | { 32 | m_qsettings = new QSettings("Serenity", "browser", this); 33 | } 34 | 35 | QString Settings::new_tab_page() 36 | { 37 | static auto const default_new_tab_url = rebase_default_url_on_serenity_resource_root(Browser::default_new_tab_url); 38 | return m_qsettings->value("new_tab_page", default_new_tab_url).toString(); 39 | } 40 | 41 | void Settings::set_new_tab_page(QString const& page) 42 | { 43 | m_qsettings->setValue("new_tab_page", page); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Settings.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Filiph Sandström 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | 12 | namespace Browser { 13 | 14 | class Settings : public QObject { 15 | public: 16 | Settings(); 17 | 18 | QString new_tab_page(); 19 | void set_new_tab_page(QString const& page); 20 | 21 | private: 22 | QSettings* m_qsettings; 23 | }; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/SettingsDialog.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Filiph Sandström 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "SettingsDialog.h" 8 | #include "Settings.h" 9 | #include 10 | #include 11 | 12 | extern Browser::Settings* s_settings; 13 | 14 | SettingsDialog::SettingsDialog(QMainWindow* window) 15 | : m_window(window) 16 | { 17 | m_layout = new QFormLayout(this); 18 | m_new_tab_page = new QLineEdit(this); 19 | m_ok_button = new QPushButton("&Save", this); 20 | 21 | m_layout->addRow(new QLabel("Page on New Tab", this), m_new_tab_page); 22 | m_layout->addWidget(m_ok_button); 23 | 24 | QObject::connect(m_ok_button, &QPushButton::released, this, [this] { 25 | close(); 26 | }); 27 | 28 | setWindowTitle("Settings"); 29 | setFixedWidth(300); 30 | setFixedHeight(150); 31 | setLayout(m_layout); 32 | show(); 33 | setFocus(); 34 | } 35 | 36 | void SettingsDialog::closeEvent(QCloseEvent* event) 37 | { 38 | save(); 39 | event->accept(); 40 | } 41 | 42 | void SettingsDialog::save() 43 | { 44 | // FIXME: Validate data. 45 | s_settings->set_new_tab_page(m_new_tab_page->text()); 46 | } 47 | -------------------------------------------------------------------------------- /src/SettingsDialog.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Filiph Sandström 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #pragma once 14 | 15 | class SettingsDialog : public QDialog { 16 | Q_OBJECT 17 | public: 18 | explicit SettingsDialog(QMainWindow* window); 19 | 20 | void save(); 21 | 22 | virtual void closeEvent(QCloseEvent*) override; 23 | 24 | private: 25 | QFormLayout* m_layout; 26 | QPushButton* m_ok_button { nullptr }; 27 | QLineEdit* m_new_tab_page { nullptr }; 28 | QMainWindow* m_window { nullptr }; 29 | }; 30 | -------------------------------------------------------------------------------- /src/Tab.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * Copyright (c) 2022, Matthew Costa 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include "Tab.h" 9 | #include "BrowserWindow.h" 10 | #include "ConsoleWidget.h" 11 | #include "InspectorWidget.h" 12 | #include "Settings.h" 13 | #include "Utilities.h" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | extern DeprecatedString s_serenity_resource_root; 33 | extern Browser::Settings* s_settings; 34 | 35 | static QIcon render_svg_icon_with_theme_colors(QString name, QPalette const& palette) 36 | { 37 | auto path = QString(":/Icons/%1.svg").arg(name); 38 | 39 | QSize icon_size(16, 16); 40 | 41 | QIcon icon; 42 | 43 | auto render = [&](QColor color) -> QPixmap { 44 | QImage image(icon_size, QImage::Format_ARGB32); 45 | image.fill(Qt::transparent); 46 | 47 | QPainter painter(&image); 48 | QSvgRenderer renderer(path); 49 | renderer.render(&painter); 50 | painter.setBrush(color); 51 | painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); 52 | painter.fillRect(image.rect(), color); 53 | return QPixmap::fromImage(image); 54 | }; 55 | 56 | icon.addPixmap(render(palette.color(QPalette::ColorGroup::Normal, QPalette::ColorRole::ButtonText)), QIcon::Mode::Normal); 57 | icon.addPixmap(render(palette.color(QPalette::ColorGroup::Disabled, QPalette::ColorRole::ButtonText)), QIcon::Mode::Disabled); 58 | 59 | return icon; 60 | } 61 | 62 | Tab::Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling enable_callgrind_profiling, WebView::UseJavaScriptBytecode use_javascript_bytecode) 63 | : QWidget(window) 64 | , m_window(window) 65 | { 66 | m_layout = new QBoxLayout(QBoxLayout::Direction::TopToBottom, this); 67 | m_layout->setSpacing(0); 68 | m_layout->setContentsMargins(0, 0, 0, 0); 69 | 70 | m_view = new WebContentView(webdriver_content_ipc_path, enable_callgrind_profiling, use_javascript_bytecode); 71 | m_toolbar = new QToolBar(this); 72 | m_location_edit = new LocationEdit(this); 73 | m_reset_zoom_button = new QToolButton(m_toolbar); 74 | 75 | m_hover_label = new QLabel(this); 76 | m_hover_label->hide(); 77 | m_hover_label->setFrameShape(QFrame::Shape::Box); 78 | m_hover_label->setAutoFillBackground(true); 79 | 80 | auto* focus_location_editor_action = new QAction("Edit Location"); 81 | focus_location_editor_action->setShortcut(QKeySequence("Ctrl+L")); 82 | addAction(focus_location_editor_action); 83 | 84 | m_layout->addWidget(m_toolbar); 85 | m_layout->addWidget(m_view); 86 | 87 | rerender_toolbar_icons(); 88 | 89 | m_toolbar->addAction(&m_window->go_back_action()); 90 | m_toolbar->addAction(&m_window->go_forward_action()); 91 | m_toolbar->addAction(&m_window->reload_action()); 92 | m_toolbar->addWidget(m_location_edit); 93 | m_reset_zoom_button->setToolTip("Reset zoom level"); 94 | m_reset_zoom_button_action = m_toolbar->addWidget(m_reset_zoom_button); 95 | m_reset_zoom_button_action->setVisible(false); 96 | 97 | QObject::connect(m_reset_zoom_button, &QAbstractButton::clicked, [this] { 98 | view().reset_zoom(); 99 | update_reset_zoom_button(); 100 | }); 101 | 102 | view().on_activate_tab = [this] { 103 | m_window->activate_tab(tab_index()); 104 | }; 105 | 106 | view().on_close = [this] { 107 | m_window->close_tab(tab_index()); 108 | }; 109 | 110 | view().on_link_hover = [this](auto const& url) { 111 | m_hover_label->setText(qstring_from_ak_deprecated_string(url.to_deprecated_string())); 112 | update_hover_label(); 113 | m_hover_label->show(); 114 | }; 115 | 116 | view().on_link_unhover = [this]() { 117 | m_hover_label->hide(); 118 | }; 119 | 120 | view().on_back_button = [this] { 121 | back(); 122 | }; 123 | 124 | view().on_forward_button = [this] { 125 | forward(); 126 | }; 127 | 128 | view().on_load_start = [this](const URL& url, bool is_redirect) { 129 | // If we are loading due to a redirect, we replace the current history entry 130 | // with the loaded URL 131 | if (is_redirect) { 132 | m_history.replace_current(url, m_title.toUtf8().data()); 133 | } 134 | 135 | m_location_edit->setText(url.to_deprecated_string().characters()); 136 | m_location_edit->setCursorPosition(0); 137 | 138 | // Don't add to history if back or forward is pressed 139 | if (!m_is_history_navigation) { 140 | m_history.push(url, m_title.toUtf8().data()); 141 | } 142 | m_is_history_navigation = false; 143 | 144 | m_window->go_back_action().setEnabled(m_history.can_go_back()); 145 | m_window->go_forward_action().setEnabled(m_history.can_go_forward()); 146 | 147 | if (m_inspector_widget) 148 | m_inspector_widget->clear_dom_json(); 149 | 150 | if (m_console_widget) 151 | m_console_widget->reset(); 152 | }; 153 | 154 | view().on_load_finish = [this](auto&) { 155 | if (m_inspector_widget != nullptr && m_inspector_widget->isVisible()) { 156 | view().inspect_dom_tree(); 157 | view().inspect_accessibility_tree(); 158 | } 159 | }; 160 | 161 | QObject::connect(m_location_edit, &QLineEdit::returnPressed, this, &Tab::location_edit_return_pressed); 162 | 163 | view().on_title_change = [this](auto const& title) { 164 | m_title = qstring_from_ak_deprecated_string(title); 165 | m_history.update_title(title); 166 | 167 | emit title_changed(tab_index(), m_title); 168 | }; 169 | 170 | view().on_favicon_change = [this](auto const& bitmap) { 171 | auto qimage = QImage(bitmap.scanline_u8(0), bitmap.width(), bitmap.height(), QImage::Format_ARGB32); 172 | if (qimage.isNull()) 173 | return; 174 | auto qpixmap = QPixmap::fromImage(qimage); 175 | if (qpixmap.isNull()) 176 | return; 177 | emit favicon_changed(tab_index(), QIcon(qpixmap)); 178 | }; 179 | 180 | QObject::connect(focus_location_editor_action, &QAction::triggered, this, &Tab::focus_location_editor); 181 | 182 | view().on_get_source = [this](auto const& url, auto const& source) { 183 | auto* text_edit = new QPlainTextEdit(this); 184 | text_edit->setWindowFlags(Qt::Window); 185 | text_edit->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont)); 186 | text_edit->resize(800, 600); 187 | text_edit->setWindowTitle(qstring_from_ak_deprecated_string(url.to_deprecated_string())); 188 | text_edit->setPlainText(qstring_from_ak_deprecated_string(source)); 189 | text_edit->show(); 190 | }; 191 | 192 | view().on_navigate_back = [this]() { 193 | back(); 194 | }; 195 | 196 | view().on_navigate_forward = [this]() { 197 | forward(); 198 | }; 199 | 200 | view().on_refresh = [this]() { 201 | reload(); 202 | }; 203 | 204 | view().on_restore_window = [this]() { 205 | m_window->showNormal(); 206 | }; 207 | 208 | view().on_reposition_window = [this](auto const& position) { 209 | m_window->move(position.x(), position.y()); 210 | return Gfx::IntPoint { m_window->x(), m_window->y() }; 211 | }; 212 | 213 | view().on_resize_window = [this](auto const& size) { 214 | m_window->resize(size.width(), size.height()); 215 | return Gfx::IntSize { m_window->width(), m_window->height() }; 216 | }; 217 | 218 | view().on_maximize_window = [this]() { 219 | m_window->showMaximized(); 220 | return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() }; 221 | }; 222 | 223 | view().on_minimize_window = [this]() { 224 | m_window->showMinimized(); 225 | return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() }; 226 | }; 227 | 228 | view().on_fullscreen_window = [this]() { 229 | m_window->showFullScreen(); 230 | return Gfx::IntRect { m_window->x(), m_window->y(), m_window->width(), m_window->height() }; 231 | }; 232 | 233 | view().on_get_dom_tree = [this](auto& dom_tree) { 234 | if (m_inspector_widget) 235 | m_inspector_widget->set_dom_json(dom_tree); 236 | }; 237 | 238 | view().on_get_accessibility_tree = [this](auto& accessibility_tree) { 239 | if (m_inspector_widget) 240 | m_inspector_widget->set_accessibility_json(accessibility_tree); 241 | }; 242 | 243 | view().on_js_console_new_message = [this](auto message_index) { 244 | if (m_console_widget) 245 | m_console_widget->notify_about_new_console_message(message_index); 246 | }; 247 | 248 | view().on_get_js_console_messages = [this](auto start_index, auto& message_types, auto& messages) { 249 | if (m_console_widget) 250 | m_console_widget->handle_console_messages(start_index, message_types, messages); 251 | }; 252 | 253 | auto* take_visible_screenshot_action = new QAction("Take &Visible Screenshot", this); 254 | take_visible_screenshot_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters()))); 255 | QObject::connect(take_visible_screenshot_action, &QAction::triggered, this, [this]() { 256 | if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Visible); result.is_error()) { 257 | auto error = String::formatted("{}", result.error()).release_value_but_fixme_should_propagate_errors(); 258 | QMessageBox::warning(this, "browser", qstring_from_ak_string(error)); 259 | } 260 | }); 261 | 262 | auto* take_full_screenshot_action = new QAction("Take &Full Screenshot", this); 263 | take_full_screenshot_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters()))); 264 | QObject::connect(take_full_screenshot_action, &QAction::triggered, this, [this]() { 265 | if (auto result = view().take_screenshot(WebView::ViewImplementation::ScreenshotType::Full); result.is_error()) { 266 | auto error = String::formatted("{}", result.error()).release_value_but_fixme_should_propagate_errors(); 267 | QMessageBox::warning(this, "browser", qstring_from_ak_string(error)); 268 | } 269 | }); 270 | 271 | m_page_context_menu = make("Context menu", this); 272 | m_page_context_menu->addAction(&m_window->go_back_action()); 273 | m_page_context_menu->addAction(&m_window->go_forward_action()); 274 | m_page_context_menu->addAction(&m_window->reload_action()); 275 | m_page_context_menu->addSeparator(); 276 | m_page_context_menu->addAction(&m_window->copy_selection_action()); 277 | m_page_context_menu->addAction(&m_window->select_all_action()); 278 | m_page_context_menu->addSeparator(); 279 | m_page_context_menu->addAction(take_visible_screenshot_action); 280 | m_page_context_menu->addAction(take_full_screenshot_action); 281 | m_page_context_menu->addSeparator(); 282 | m_page_context_menu->addAction(&m_window->view_source_action()); 283 | m_page_context_menu->addAction(&m_window->inspect_dom_node_action()); 284 | 285 | view().on_context_menu_request = [this](Gfx::IntPoint) { 286 | auto screen_position = QCursor::pos(); 287 | m_page_context_menu->exec(screen_position); 288 | }; 289 | 290 | auto* open_link_action = new QAction("&Open", this); 291 | open_link_action->setIcon(QIcon(QString("%1/res/icons/16x16/go-forward.png").arg(s_serenity_resource_root.characters()))); 292 | QObject::connect(open_link_action, &QAction::triggered, this, [this]() { 293 | open_link(m_link_context_menu_url); 294 | }); 295 | 296 | auto* open_link_in_new_tab_action = new QAction("&Open in New &Tab", this); 297 | open_link_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters()))); 298 | QObject::connect(open_link_in_new_tab_action, &QAction::triggered, this, [this]() { 299 | open_link_in_new_tab(m_link_context_menu_url); 300 | }); 301 | 302 | auto* copy_url_action = new QAction("Copy &URL", this); 303 | copy_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters()))); 304 | QObject::connect(copy_url_action, &QAction::triggered, this, [this]() { 305 | copy_link_url(m_link_context_menu_url); 306 | }); 307 | 308 | m_link_context_menu = make("Link context menu", this); 309 | m_link_context_menu->addAction(open_link_action); 310 | m_link_context_menu->addAction(open_link_in_new_tab_action); 311 | m_link_context_menu->addSeparator(); 312 | m_link_context_menu->addAction(copy_url_action); 313 | m_link_context_menu->addSeparator(); 314 | m_link_context_menu->addAction(&m_window->inspect_dom_node_action()); 315 | 316 | view().on_link_context_menu_request = [this](auto const& url, Gfx::IntPoint) { 317 | m_link_context_menu_url = url; 318 | 319 | auto screen_position = QCursor::pos(); 320 | m_link_context_menu->exec(screen_position); 321 | }; 322 | 323 | auto* open_image_action = new QAction("&Open Image", this); 324 | open_image_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-image.png").arg(s_serenity_resource_root.characters()))); 325 | QObject::connect(open_image_action, &QAction::triggered, this, [this]() { 326 | open_link(m_image_context_menu_url); 327 | }); 328 | 329 | auto* open_image_in_new_tab_action = new QAction("&Open Image in New &Tab", this); 330 | open_image_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters()))); 331 | QObject::connect(open_image_in_new_tab_action, &QAction::triggered, this, [this]() { 332 | open_link_in_new_tab(m_image_context_menu_url); 333 | }); 334 | 335 | auto* copy_image_action = new QAction("&Copy Image", this); 336 | copy_image_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters()))); 337 | QObject::connect(copy_image_action, &QAction::triggered, this, [this]() { 338 | auto* bitmap = m_image_context_menu_bitmap.bitmap(); 339 | if (bitmap == nullptr) 340 | return; 341 | 342 | auto data = Gfx::BMPWriter::encode(*bitmap); 343 | if (data.is_error()) 344 | return; 345 | 346 | auto image = QImage::fromData(data.value().data(), data.value().size(), "BMP"); 347 | if (image.isNull()) 348 | return; 349 | 350 | auto* clipboard = QGuiApplication::clipboard(); 351 | clipboard->setImage(image); 352 | }); 353 | 354 | auto* copy_image_url_action = new QAction("Copy Image &URL", this); 355 | copy_image_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters()))); 356 | QObject::connect(copy_image_url_action, &QAction::triggered, this, [this]() { 357 | copy_link_url(m_image_context_menu_url); 358 | }); 359 | 360 | m_image_context_menu = make("Image context menu", this); 361 | m_image_context_menu->addAction(open_image_action); 362 | m_image_context_menu->addAction(open_image_in_new_tab_action); 363 | m_image_context_menu->addSeparator(); 364 | m_image_context_menu->addAction(copy_image_action); 365 | m_image_context_menu->addAction(copy_image_url_action); 366 | m_image_context_menu->addSeparator(); 367 | m_image_context_menu->addAction(&m_window->inspect_dom_node_action()); 368 | 369 | view().on_image_context_menu_request = [this](auto& image_url, Gfx::IntPoint, Gfx::ShareableBitmap const& shareable_bitmap) { 370 | m_image_context_menu_url = image_url; 371 | m_image_context_menu_bitmap = shareable_bitmap; 372 | 373 | auto screen_position = QCursor::pos(); 374 | m_image_context_menu->exec(screen_position); 375 | }; 376 | 377 | m_media_context_menu_play_icon = make(QString("%1/res/icons/16x16/play.png").arg(s_serenity_resource_root.characters())); 378 | m_media_context_menu_pause_icon = make(QString("%1/res/icons/16x16/pause.png").arg(s_serenity_resource_root.characters())); 379 | m_media_context_menu_mute_icon = make(QString("%1/res/icons/16x16/audio-volume-muted.png").arg(s_serenity_resource_root.characters())); 380 | m_media_context_menu_unmute_icon = make(QString("%1/res/icons/16x16/audio-volume-high.png").arg(s_serenity_resource_root.characters())); 381 | 382 | m_media_context_menu_play_pause_action = make("&Play", this); 383 | m_media_context_menu_play_pause_action->setIcon(*m_media_context_menu_play_icon); 384 | QObject::connect(m_media_context_menu_play_pause_action, &QAction::triggered, this, [this]() { 385 | view().toggle_media_play_state(); 386 | }); 387 | 388 | m_media_context_menu_mute_unmute_action = make("&Mute", this); 389 | m_media_context_menu_mute_unmute_action->setIcon(*m_media_context_menu_mute_icon); 390 | QObject::connect(m_media_context_menu_mute_unmute_action, &QAction::triggered, this, [this]() { 391 | view().toggle_media_mute_state(); 392 | }); 393 | 394 | m_media_context_menu_controls_action = make("Show &Controls", this); 395 | m_media_context_menu_controls_action->setCheckable(true); 396 | QObject::connect(m_media_context_menu_controls_action, &QAction::triggered, this, [this]() { 397 | view().toggle_media_controls_state(); 398 | }); 399 | 400 | m_media_context_menu_loop_action = make("&Loop", this); 401 | m_media_context_menu_loop_action->setCheckable(true); 402 | QObject::connect(m_media_context_menu_loop_action, &QAction::triggered, this, [this]() { 403 | view().toggle_media_loop_state(); 404 | }); 405 | 406 | auto* open_audio_action = new QAction("&Open Audio", this); 407 | open_audio_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-sound.png").arg(s_serenity_resource_root.characters()))); 408 | QObject::connect(open_audio_action, &QAction::triggered, this, [this]() { 409 | open_link(m_media_context_menu_url); 410 | }); 411 | 412 | auto* open_audio_in_new_tab_action = new QAction("Open Audio in New &Tab", this); 413 | open_audio_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters()))); 414 | QObject::connect(open_audio_in_new_tab_action, &QAction::triggered, this, [this]() { 415 | open_link_in_new_tab(m_media_context_menu_url); 416 | }); 417 | 418 | auto* copy_audio_url_action = new QAction("Copy Audio &URL", this); 419 | copy_audio_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters()))); 420 | QObject::connect(copy_audio_url_action, &QAction::triggered, this, [this]() { 421 | copy_link_url(m_media_context_menu_url); 422 | }); 423 | 424 | m_audio_context_menu = make("Audio context menu", this); 425 | m_audio_context_menu->addAction(m_media_context_menu_play_pause_action); 426 | m_audio_context_menu->addAction(m_media_context_menu_mute_unmute_action); 427 | m_audio_context_menu->addAction(m_media_context_menu_controls_action); 428 | m_audio_context_menu->addAction(m_media_context_menu_loop_action); 429 | m_audio_context_menu->addSeparator(); 430 | m_audio_context_menu->addAction(open_audio_action); 431 | m_audio_context_menu->addAction(open_audio_in_new_tab_action); 432 | m_audio_context_menu->addSeparator(); 433 | m_audio_context_menu->addAction(copy_audio_url_action); 434 | m_audio_context_menu->addSeparator(); 435 | m_audio_context_menu->addAction(&m_window->inspect_dom_node_action()); 436 | 437 | auto* open_video_action = new QAction("&Open Video", this); 438 | open_video_action->setIcon(QIcon(QString("%1/res/icons/16x16/filetype-video.png").arg(s_serenity_resource_root.characters()))); 439 | QObject::connect(open_video_action, &QAction::triggered, this, [this]() { 440 | open_link(m_media_context_menu_url); 441 | }); 442 | 443 | auto* open_video_in_new_tab_action = new QAction("Open Video in New &Tab", this); 444 | open_video_in_new_tab_action->setIcon(QIcon(QString("%1/res/icons/16x16/new-tab.png").arg(s_serenity_resource_root.characters()))); 445 | QObject::connect(open_video_in_new_tab_action, &QAction::triggered, this, [this]() { 446 | open_link_in_new_tab(m_media_context_menu_url); 447 | }); 448 | 449 | auto* copy_video_url_action = new QAction("Copy Video &URL", this); 450 | copy_video_url_action->setIcon(QIcon(QString("%1/res/icons/16x16/edit-copy.png").arg(s_serenity_resource_root.characters()))); 451 | QObject::connect(copy_video_url_action, &QAction::triggered, this, [this]() { 452 | copy_link_url(m_media_context_menu_url); 453 | }); 454 | 455 | m_video_context_menu = make("Video context menu", this); 456 | m_video_context_menu->addAction(m_media_context_menu_play_pause_action); 457 | m_video_context_menu->addAction(m_media_context_menu_mute_unmute_action); 458 | m_video_context_menu->addAction(m_media_context_menu_controls_action); 459 | m_video_context_menu->addAction(m_media_context_menu_loop_action); 460 | m_video_context_menu->addSeparator(); 461 | m_video_context_menu->addAction(open_video_action); 462 | m_video_context_menu->addAction(open_video_in_new_tab_action); 463 | m_video_context_menu->addSeparator(); 464 | m_video_context_menu->addAction(copy_video_url_action); 465 | m_video_context_menu->addSeparator(); 466 | m_video_context_menu->addAction(&m_window->inspect_dom_node_action()); 467 | 468 | view().on_media_context_menu_request = [this](Gfx::IntPoint, Web::Page::MediaContextMenu const& menu) { 469 | m_media_context_menu_url = menu.media_url; 470 | 471 | if (menu.is_playing) { 472 | m_media_context_menu_play_pause_action->setIcon(*m_media_context_menu_pause_icon); 473 | m_media_context_menu_play_pause_action->setText("&Pause"); 474 | } else { 475 | m_media_context_menu_play_pause_action->setIcon(*m_media_context_menu_play_icon); 476 | m_media_context_menu_play_pause_action->setText("&Play"); 477 | } 478 | 479 | if (menu.is_muted) { 480 | m_media_context_menu_mute_unmute_action->setIcon(*m_media_context_menu_unmute_icon); 481 | m_media_context_menu_mute_unmute_action->setText("Un&mute"); 482 | } else { 483 | m_media_context_menu_mute_unmute_action->setIcon(*m_media_context_menu_mute_icon); 484 | m_media_context_menu_mute_unmute_action->setText("&Mute"); 485 | } 486 | 487 | m_media_context_menu_controls_action->setChecked(menu.has_user_agent_controls); 488 | m_media_context_menu_loop_action->setChecked(menu.is_looping); 489 | 490 | auto screen_position = QCursor::pos(); 491 | 492 | if (menu.is_video) 493 | m_video_context_menu->exec(screen_position); 494 | else 495 | m_audio_context_menu->exec(screen_position); 496 | }; 497 | } 498 | 499 | Tab::~Tab() 500 | { 501 | close_sub_widgets(); 502 | } 503 | 504 | void Tab::update_reset_zoom_button() 505 | { 506 | auto zoom_level = view().zoom_level(); 507 | if (zoom_level != 1.0f) { 508 | auto zoom_level_text = MUST(String::formatted("{}%", round_to(zoom_level * 100))); 509 | m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text)); 510 | m_reset_zoom_button_action->setVisible(true); 511 | } else { 512 | m_reset_zoom_button_action->setVisible(false); 513 | } 514 | } 515 | 516 | void Tab::focus_location_editor() 517 | { 518 | m_location_edit->setFocus(); 519 | m_location_edit->selectAll(); 520 | } 521 | 522 | void Tab::navigate(QString url, LoadType load_type) 523 | { 524 | if (url.startsWith("/")) 525 | url = "file://" + url; 526 | else if (!url.startsWith("http://", Qt::CaseInsensitive) && !url.startsWith("https://", Qt::CaseInsensitive) && !url.startsWith("file://", Qt::CaseInsensitive) && !url.startsWith("about:", Qt::CaseInsensitive)) 527 | url = "https://" + url; 528 | m_is_history_navigation = (load_type == LoadType::HistoryNavigation); 529 | view().load(ak_deprecated_string_from_qstring(url)); 530 | } 531 | 532 | void Tab::back() 533 | { 534 | if (!m_history.can_go_back()) 535 | return; 536 | 537 | m_is_history_navigation = true; 538 | m_history.go_back(); 539 | view().load(m_history.current().url.to_deprecated_string()); 540 | } 541 | 542 | void Tab::forward() 543 | { 544 | if (!m_history.can_go_forward()) 545 | return; 546 | 547 | m_is_history_navigation = true; 548 | m_history.go_forward(); 549 | view().load(m_history.current().url.to_deprecated_string()); 550 | } 551 | 552 | void Tab::reload() 553 | { 554 | m_is_history_navigation = true; 555 | view().load(m_history.current().url.to_deprecated_string()); 556 | } 557 | 558 | void Tab::open_link(URL const& url) 559 | { 560 | view().on_link_click(url, "", 0); 561 | } 562 | 563 | void Tab::open_link_in_new_tab(URL const& url) 564 | { 565 | view().on_link_click(url, "_blank", 0); 566 | } 567 | 568 | void Tab::copy_link_url(URL const& url) 569 | { 570 | auto* clipboard = QGuiApplication::clipboard(); 571 | clipboard->setText(qstring_from_ak_deprecated_string(url.to_deprecated_string())); 572 | } 573 | 574 | void Tab::location_edit_return_pressed() 575 | { 576 | navigate(m_location_edit->text()); 577 | } 578 | 579 | void Tab::open_file() 580 | { 581 | auto filename = QFileDialog::getOpenFileName(this, "Open file", QDir::homePath(), "All Files (*.*)"); 582 | if (!filename.isNull()) 583 | navigate("file://" + filename); 584 | } 585 | 586 | int Tab::tab_index() 587 | { 588 | return m_window->tab_index(this); 589 | } 590 | 591 | void Tab::debug_request(DeprecatedString const& request, DeprecatedString const& argument) 592 | { 593 | if (request == "dump-history") 594 | m_history.dump(); 595 | else 596 | m_view->debug_request(request, argument); 597 | } 598 | 599 | void Tab::resizeEvent(QResizeEvent* event) 600 | { 601 | QWidget::resizeEvent(event); 602 | if (m_hover_label->isVisible()) 603 | update_hover_label(); 604 | } 605 | 606 | void Tab::update_hover_label() 607 | { 608 | m_hover_label->resize(QFontMetrics(m_hover_label->font()).boundingRect(m_hover_label->text()).adjusted(-4, -2, 4, 2).size()); 609 | m_hover_label->move(6, height() - m_hover_label->height() - 8); 610 | m_hover_label->raise(); 611 | } 612 | 613 | bool Tab::event(QEvent* event) 614 | { 615 | if (event->type() == QEvent::PaletteChange) { 616 | rerender_toolbar_icons(); 617 | return QWidget::event(event); 618 | } 619 | 620 | return QWidget::event(event); 621 | } 622 | 623 | void Tab::rerender_toolbar_icons() 624 | { 625 | m_window->go_back_action().setIcon(render_svg_icon_with_theme_colors("back", palette())); 626 | m_window->go_forward_action().setIcon(render_svg_icon_with_theme_colors("forward", palette())); 627 | m_window->reload_action().setIcon(render_svg_icon_with_theme_colors("reload", palette())); 628 | } 629 | 630 | void Tab::show_inspector_window(InspectorTarget inspector_target) 631 | { 632 | bool inspector_previously_loaded = m_inspector_widget != nullptr; 633 | 634 | if (!m_inspector_widget) { 635 | m_inspector_widget = new Ladybird::InspectorWidget; 636 | m_inspector_widget->setWindowTitle("Inspector"); 637 | m_inspector_widget->resize(640, 480); 638 | m_inspector_widget->on_close = [this] { 639 | view().clear_inspected_dom_node(); 640 | }; 641 | 642 | m_inspector_widget->on_dom_node_inspected = [&](auto id, auto pseudo_element) { 643 | return view().inspect_dom_node(id, pseudo_element); 644 | }; 645 | } 646 | 647 | if (!inspector_previously_loaded || !m_inspector_widget->dom_loaded()) { 648 | view().inspect_dom_tree(); 649 | view().inspect_accessibility_tree(); 650 | } 651 | 652 | m_inspector_widget->show(); 653 | 654 | if (inspector_target == InspectorTarget::HoveredElement) { 655 | auto hovered_node = view().get_hovered_node_id(); 656 | m_inspector_widget->set_selection({ hovered_node }); 657 | } else { 658 | m_inspector_widget->select_default_node(); 659 | } 660 | } 661 | 662 | void Tab::show_console_window() 663 | { 664 | if (!m_console_widget) { 665 | m_console_widget = new Ladybird::ConsoleWidget; 666 | m_console_widget->setWindowTitle("JS Console"); 667 | m_console_widget->resize(640, 480); 668 | m_console_widget->on_js_input = [this](auto js_source) { 669 | view().js_console_input(js_source); 670 | }; 671 | m_console_widget->on_request_messages = [this](i32 start_index) { 672 | view().js_console_request_messages(start_index); 673 | }; 674 | } 675 | 676 | m_console_widget->show(); 677 | } 678 | 679 | void Tab::close_sub_widgets() 680 | { 681 | auto close_widget_window = [](auto* widget) { 682 | if (widget) 683 | widget->close(); 684 | }; 685 | 686 | close_widget_window(m_console_widget); 687 | close_widget_window(m_inspector_widget); 688 | } 689 | -------------------------------------------------------------------------------- /src/Tab.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * Copyright (c) 2022, Matthew Costa 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include "LocationEdit.h" 11 | #include "WebContentView.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | class BrowserWindow; 21 | 22 | namespace Ladybird { 23 | class ConsoleWidget; 24 | class InspectorWidget; 25 | } 26 | 27 | class Tab final : public QWidget { 28 | Q_OBJECT 29 | public: 30 | Tab(BrowserWindow* window, StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode); 31 | virtual ~Tab() override; 32 | 33 | WebContentView& view() { return *m_view; } 34 | 35 | enum class LoadType { 36 | Normal, 37 | HistoryNavigation, 38 | }; 39 | void navigate(QString, LoadType = LoadType::Normal); 40 | void back(); 41 | void forward(); 42 | void reload(); 43 | 44 | void debug_request(DeprecatedString const& request, DeprecatedString const& argument); 45 | 46 | void open_file(); 47 | void update_reset_zoom_button(); 48 | 49 | enum class InspectorTarget { 50 | Document, 51 | HoveredElement 52 | }; 53 | void show_inspector_window(InspectorTarget = InspectorTarget::Document); 54 | void show_console_window(); 55 | 56 | Ladybird::ConsoleWidget* console() { return m_console_widget; }; 57 | 58 | public slots: 59 | void focus_location_editor(); 60 | void location_edit_return_pressed(); 61 | 62 | signals: 63 | void title_changed(int id, QString); 64 | void favicon_changed(int id, QIcon); 65 | 66 | private: 67 | virtual void resizeEvent(QResizeEvent*) override; 68 | virtual bool event(QEvent*) override; 69 | 70 | void rerender_toolbar_icons(); 71 | void update_hover_label(); 72 | 73 | void open_link(URL const&); 74 | void open_link_in_new_tab(URL const&); 75 | void copy_link_url(URL const&); 76 | 77 | void close_sub_widgets(); 78 | 79 | QBoxLayout* m_layout; 80 | QToolBar* m_toolbar { nullptr }; 81 | QToolButton* m_reset_zoom_button { nullptr }; 82 | QAction* m_reset_zoom_button_action { nullptr }; 83 | LocationEdit* m_location_edit { nullptr }; 84 | WebContentView* m_view { nullptr }; 85 | BrowserWindow* m_window { nullptr }; 86 | Browser::History m_history; 87 | QString m_title; 88 | QLabel* m_hover_label { nullptr }; 89 | 90 | OwnPtr m_page_context_menu; 91 | 92 | OwnPtr m_link_context_menu; 93 | URL m_link_context_menu_url; 94 | 95 | OwnPtr m_image_context_menu; 96 | Gfx::ShareableBitmap m_image_context_menu_bitmap; 97 | URL m_image_context_menu_url; 98 | 99 | OwnPtr m_audio_context_menu; 100 | OwnPtr m_video_context_menu; 101 | OwnPtr m_media_context_menu_play_icon; 102 | OwnPtr m_media_context_menu_pause_icon; 103 | OwnPtr m_media_context_menu_mute_icon; 104 | OwnPtr m_media_context_menu_unmute_icon; 105 | OwnPtr m_media_context_menu_play_pause_action; 106 | OwnPtr m_media_context_menu_mute_unmute_action; 107 | OwnPtr m_media_context_menu_controls_action; 108 | OwnPtr m_media_context_menu_loop_action; 109 | URL m_media_context_menu_url; 110 | 111 | int tab_index(); 112 | 113 | bool m_is_history_navigation { false }; 114 | 115 | Ladybird::ConsoleWidget* m_console_widget { nullptr }; 116 | Ladybird::InspectorWidget* m_inspector_widget { nullptr }; 117 | }; 118 | -------------------------------------------------------------------------------- /src/Utilities.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "Utilities.h" 8 | #include 9 | #include 10 | #include 11 | 12 | DeprecatedString s_serenity_resource_root; 13 | 14 | AK::DeprecatedString ak_deprecated_string_from_cstring(const char *cstring) 15 | { 16 | return AK::DeprecatedString(cstring); 17 | } 18 | 19 | ErrorOr ak_string_from_cstring(const char *cstring) 20 | { 21 | return String::from_utf8(StringView(cstring, strlen(cstring))); 22 | } 23 | 24 | ErrorOr ak_deprecated_string_from_ustring(Glib::ustring ustring) 25 | { 26 | return ak_deprecated_string_from_cstring(ustring.c_str()); 27 | } 28 | 29 | ErrorOr ak_string_from_ustring(Glib::ustring ustring) 30 | { 31 | return ak_string_from_cstring(ustring.c_str()); 32 | } 33 | 34 | Glib::ustring ustring_from_ak_deprecated_string(AK::DeprecatedString const& ak_deprecated_string) 35 | { 36 | return {ak_deprecated_string.characters(), ak_deprecated_string.length()}; 37 | } 38 | 39 | Glib::ustring ustring_from_ak_string(String const& ak_string) 40 | { 41 | auto view = ak_string.bytes_as_string_view(); 42 | return {view.characters_without_null_termination(), view.length()}; 43 | } 44 | 45 | char* owned_cstring_from_ak_string(String const& ak_string) 46 | { 47 | auto view = ak_string.bytes_as_string_view(); 48 | return g_strndup(view.characters_without_null_termination(), view.length()); 49 | } 50 | 51 | void platform_init() 52 | { 53 | #ifdef AK_OS_ANDROID 54 | extern void android_platform_init(); 55 | android_platform_init(); 56 | #else 57 | s_serenity_resource_root = [] { 58 | auto const* source_dir = getenv("SERENITY_SOURCE_DIR"); 59 | if (source_dir) { 60 | return DeprecatedString::formatted("{}/Base", source_dir); 61 | } 62 | auto* home = getenv("XDG_CONFIG_HOME") ?: getenv("HOME"); 63 | VERIFY(home); 64 | auto home_lagom = DeprecatedString::formatted("{}/.lagom", home); 65 | if (FileSystem::is_directory(home_lagom)) 66 | return home_lagom; 67 | auto app_dir = ak_deprecated_string_from_cstring(g_get_current_dir()); 68 | # ifdef AK_OS_MACOS 69 | return LexicalPath(app_dir).parent().append("Resources"sv).string(); 70 | # else 71 | return LexicalPath(app_dir).parent().append("share"sv).string(); 72 | # endif 73 | }(); 74 | #endif 75 | } 76 | -------------------------------------------------------------------------------- /src/Utilities.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Andreas Kling 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | AK::DeprecatedString ak_deprecated_string_from_cstring(const char *cstring); 14 | ErrorOr ak_string_from_cstring(const char *cstring); 15 | ErrorOr ak_deprecated_string_from_ustring(Glib::ustring ustring); 16 | ErrorOr ak_string_from_ustring(Glib::ustring ustring); 17 | Glib::ustring ustring_from_ak_deprecated_string(AK::DeprecatedString const& ak_deprecated_string); 18 | Glib::ustring ustring_from_ak_string(String const& ak_string); 19 | char* owned_cstring_from_ak_string(String const& ak_string); 20 | void platform_init(); 21 | 22 | extern DeprecatedString s_serenity_resource_root; 23 | -------------------------------------------------------------------------------- /src/WebContent/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(WEBCONTENT_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Services/WebContent/) 2 | 3 | set(WEBCONTENT_SOURCES 4 | ${WEBCONTENT_SOURCE_DIR}/ConnectionFromClient.cpp 5 | ${WEBCONTENT_SOURCE_DIR}/ConsoleGlobalEnvironmentExtensions.cpp 6 | ${WEBCONTENT_SOURCE_DIR}/PageHost.cpp 7 | ${WEBCONTENT_SOURCE_DIR}/WebContentConsoleClient.cpp 8 | ${WEBCONTENT_SOURCE_DIR}/WebDriverConnection.cpp 9 | ../AudioCodecPluginLadybird.cpp 10 | ../EventLoopImplementationGLib.cpp 11 | ../FontPluginPango.cpp 12 | ../ImageCodecPluginLadybird.cpp 13 | ../RequestManagerSoup.cpp 14 | ../Utilities.cpp 15 | #../WebSocketClientManagerLadybird.cpp 16 | #../WebSocketLadybird.cpp 17 | #../WebSocketImplQt.cpp 18 | main.cpp 19 | ) 20 | 21 | if (APPLE) 22 | list(APPEND WEBCONTENT_SOURCES MacOSSetup.mm) 23 | find_library(COCOA_LIBRARY Cocoa) 24 | if(NOT COCOA_LIBRARY) 25 | message(FATAL_ERROR "Cocoa framework not found") 26 | endif() 27 | endif() 28 | 29 | add_executable(WebContent ${WEBCONTENT_SOURCES}) 30 | 31 | target_include_directories(WebContent PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services/) 32 | target_include_directories(WebContent PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..) 33 | target_link_libraries(WebContent PRIVATE ${GTK4_LIBRARIES} ${SOUP3_LIBRARIES} ${COCOA_LIBRARY} LibAudio LibCore LibFileSystem LibGfx LibIPC LibJS LibMain LibWeb LibWebSocket) 34 | -------------------------------------------------------------------------------- /src/WebContent/MacOSSetup.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Tim Flynn 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #pragma once 8 | 9 | void prohibit_interaction(); 10 | -------------------------------------------------------------------------------- /src/WebContent/MacOSSetup.mm: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Tim Flynn 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "MacOSSetup.h" 8 | #import 9 | 10 | void prohibit_interaction() 11 | { 12 | // This prevents WebContent from being displayed in the macOS Dock and becoming the focused, 13 | // interactable application upon launch. 14 | [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; 15 | } 16 | -------------------------------------------------------------------------------- /src/WebContent/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2023, Andreas Kling 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "../AudioCodecPluginLadybird.h" 8 | #include "../EventLoopImplementationGLib.h" 9 | #include "../FontPluginPango.h" 10 | #include "../ImageCodecPluginLadybird.h" 11 | #include "../RequestManagerSoup.h" 12 | #include "../Utilities.h" 13 | // #include "../WebSocketClientManagerLadybird.h" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #if defined(AK_OS_MACOS) 38 | # include "MacOSSetup.h" 39 | #include "../RequestManagerSoup.h" 40 | 41 | #endif 42 | 43 | static ErrorOr load_content_filters(); 44 | static ErrorOr load_autoplay_allowlist(); 45 | 46 | extern DeprecatedString s_serenity_resource_root; 47 | 48 | ErrorOr serenity_main(Main::Arguments arguments) 49 | { 50 | // QGuiApplication app(arguments.argc, arguments.argv); 51 | Gtk::Application::create("com.mattjakeman.LibWebGTK.WebContent"); 52 | 53 | #if defined(AK_OS_MACOS) 54 | prohibit_interaction(); 55 | #endif 56 | 57 | Core::EventLoopManager::install(*new Ladybird::EventLoopManagerGLib); 58 | Core::EventLoop event_loop; 59 | 60 | platform_init(); 61 | 62 | Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPluginSerenity); 63 | Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPluginLadybird); 64 | 65 | Web::Platform::AudioCodecPlugin::install_creation_hook([](auto loader) { 66 | return Ladybird::AudioCodecPluginLadybird::create(move(loader)); 67 | }); 68 | 69 | // TODO: WE DEFINITELY NEED THESE !! 70 | Web::ResourceLoader::initialize(RequestManagerSoup::create()); 71 | //Web::WebSockets::WebSocketClientManager::initialize(Ladybird::WebSocketClientManagerLadybird::create()); 72 | 73 | Web::FrameLoader::set_default_favicon_path(DeprecatedString::formatted("{}/res/icons/16x16/app-browser.png", s_serenity_resource_root)); 74 | 75 | int webcontent_fd_passing_socket { -1 }; 76 | bool is_layout_test_mode = false; 77 | bool use_javascript_bytecode = false; 78 | 79 | Core::ArgsParser args_parser; 80 | args_parser.add_option(webcontent_fd_passing_socket, "File descriptor of the passing socket for the WebContent connection", "webcontent-fd-passing-socket", 'c', "webcontent_fd_passing_socket"); 81 | args_parser.add_option(is_layout_test_mode, "Is layout test mode", "layout-test-mode", 0); 82 | args_parser.add_option(use_javascript_bytecode, "Enable JavaScript bytecode VM", "use-bytecode", 0); 83 | args_parser.parse(arguments); 84 | 85 | JS::Bytecode::Interpreter::set_enabled(use_javascript_bytecode); 86 | 87 | VERIFY(webcontent_fd_passing_socket >= 0); 88 | 89 | Web::Platform::FontPlugin::install(*new Ladybird::FontPluginGTK(is_layout_test_mode)); 90 | 91 | Web::FrameLoader::set_error_page_url(DeprecatedString::formatted("file://{}/res/html/error.html", s_serenity_resource_root)); 92 | 93 | TRY(Web::Bindings::initialize_main_thread_vm()); 94 | 95 | auto maybe_content_filter_error = load_content_filters(); 96 | if (maybe_content_filter_error.is_error()) 97 | dbgln("Failed to load content filters: {}", maybe_content_filter_error.error()); 98 | 99 | auto maybe_autoplay_allowlist_error = load_autoplay_allowlist(); 100 | if (maybe_autoplay_allowlist_error.is_error()) 101 | dbgln("Failed to load autoplay allowlist: {}", maybe_autoplay_allowlist_error.error()); 102 | 103 | auto webcontent_socket = TRY(Core::take_over_socket_from_system_server("WebContent"sv)); 104 | auto webcontent_client = TRY(WebContent::ConnectionFromClient::try_create(move(webcontent_socket))); 105 | webcontent_client->set_fd_passing_socket(TRY(Core::LocalSocket::adopt_fd(webcontent_fd_passing_socket))); 106 | 107 | return event_loop.exec(); 108 | } 109 | 110 | static ErrorOr load_content_filters() 111 | { 112 | auto file_or_error = Core::File::open(DeprecatedString::formatted("{}/home/anon/.config/BrowserContentFilters.txt", s_serenity_resource_root), Core::File::OpenMode::Read); 113 | if (file_or_error.is_error()) 114 | file_or_error = Core::File::open(DeprecatedString::formatted("{}/res/ladybird/BrowserContentFilters.txt", s_serenity_resource_root), Core::File::OpenMode::Read); 115 | if (file_or_error.is_error()) 116 | return file_or_error.release_error(); 117 | 118 | auto file = file_or_error.release_value(); 119 | auto ad_filter_list = TRY(Core::InputBufferedFile::create(move(file))); 120 | auto buffer = TRY(ByteBuffer::create_uninitialized(4096)); 121 | 122 | Vector patterns; 123 | 124 | while (TRY(ad_filter_list->can_read_line())) { 125 | auto line = TRY(ad_filter_list->read_line(buffer)); 126 | if (line.is_empty()) 127 | continue; 128 | 129 | auto pattern = TRY(String::from_utf8(line)); 130 | TRY(patterns.try_append(move(pattern))); 131 | } 132 | 133 | auto& content_filter = Web::ContentFilter::the(); 134 | TRY(content_filter.set_patterns(patterns)); 135 | 136 | return {}; 137 | } 138 | 139 | static ErrorOr load_autoplay_allowlist() 140 | { 141 | auto file_or_error = Core::File::open(TRY(String::formatted("{}/home/anon/.config/BrowserAutoplayAllowlist.txt", s_serenity_resource_root)), Core::File::OpenMode::Read); 142 | if (file_or_error.is_error()) 143 | file_or_error = Core::File::open(TRY(String::formatted("{}/res/ladybird/BrowserAutoplayAllowlist.txt", s_serenity_resource_root)), Core::File::OpenMode::Read); 144 | if (file_or_error.is_error()) 145 | return file_or_error.release_error(); 146 | 147 | auto file = file_or_error.release_value(); 148 | auto allowlist = TRY(Core::InputBufferedFile::create(move(file))); 149 | auto buffer = TRY(ByteBuffer::create_uninitialized(4096)); 150 | 151 | Vector origins; 152 | 153 | while (TRY(allowlist->can_read_line())) { 154 | auto line = TRY(allowlist->read_line(buffer)); 155 | if (line.is_empty()) 156 | continue; 157 | 158 | auto domain = TRY(String::from_utf8(line)); 159 | TRY(origins.try_append(move(domain))); 160 | } 161 | 162 | auto& autoplay_allowlist = Web::PermissionsPolicy::AutoplayAllowlist::the(); 163 | TRY(autoplay_allowlist.enable_for_origins(origins)); 164 | 165 | return {}; 166 | } 167 | -------------------------------------------------------------------------------- /src/WebContentView.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022-2023, Andreas Kling 3 | * Copyright (c) 2023, Linus Groh 4 | * Copyright (c) 2023, Matthew Jakeman 5 | * 6 | * SPDX-License-Identifier: BSD-2-Clause 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | namespace WebView { 39 | class WebContentClient; 40 | } 41 | 42 | using WebView::WebContentClient; 43 | 44 | class Tab; 45 | 46 | class WebContentView final 47 | : public Gtk::Scrollable // must come BEFORE Widget (so interface can be init'd) 48 | , public Gtk::Widget 49 | , public WebView::ViewImplementation { 50 | public: 51 | explicit WebContentView(StringView webdriver_content_ipc_path, WebView::EnableCallgrindProfiling, WebView::UseJavaScriptBytecode); 52 | virtual ~WebContentView() override; 53 | 54 | Function on_tab_open_request; 55 | 56 | virtual void snapshot_vfunc(const Glib::RefPtr& snapshot) override; 57 | virtual void size_allocate_vfunc(int width, int height, int baseline) override; 58 | 59 | /*virtual void paintEvent(QPaintEvent*) override; 60 | virtual void resizeEvent(QResizeEvent*) override; 61 | virtual void mouseMoveEvent(QMouseEvent*) override; 62 | virtual void mousePressEvent(QMouseEvent*) override; 63 | virtual void mouseReleaseEvent(QMouseEvent*) override; 64 | virtual void mouseDoubleClickEvent(QMouseEvent*) override; 65 | virtual void dragEnterEvent(QDragEnterEvent*) override; 66 | virtual void dropEvent(QDropEvent*) override; 67 | virtual void keyPressEvent(QKeyEvent* event) override; 68 | virtual void keyReleaseEvent(QKeyEvent* event) override; 69 | virtual void showEvent(QShowEvent*) override; 70 | virtual void hideEvent(QHideEvent*) override; 71 | virtual void focusInEvent(QFocusEvent*) override; 72 | virtual void focusOutEvent(QFocusEvent*) override; 73 | virtual bool event(QEvent*) override;*/ 74 | 75 | void show_event(); 76 | void hide_event(); 77 | void resize_event(int width, int height); 78 | 79 | ErrorOr dump_layout_tree(); 80 | 81 | void set_viewport_rect(Gfx::IntRect); 82 | void set_window_size(Gfx::IntSize); 83 | void set_window_position(Gfx::IntPoint); 84 | 85 | enum class PaletteMode { 86 | Default, 87 | Dark, 88 | }; 89 | void update_palette(PaletteMode = PaletteMode::Default); 90 | 91 | virtual void notify_server_did_layout(Badge, Gfx::IntSize content_size) override; 92 | virtual void notify_server_did_paint(Badge, i32 bitmap_id, Gfx::IntSize) override; 93 | virtual void notify_server_did_invalidate_content_rect(Badge, Gfx::IntRect const&) override; 94 | virtual void notify_server_did_change_selection(Badge) override; 95 | virtual void notify_server_did_request_cursor_change(Badge, Gfx::StandardCursor cursor) override; 96 | virtual void notify_server_did_request_scroll(Badge, i32, i32) override; 97 | virtual void notify_server_did_request_scroll_to(Badge, Gfx::IntPoint) override; 98 | virtual void notify_server_did_request_scroll_into_view(Badge, Gfx::IntRect const&) override; 99 | virtual void notify_server_did_enter_tooltip_area(Badge, Gfx::IntPoint, DeprecatedString const&) override; 100 | virtual void notify_server_did_leave_tooltip_area(Badge) override; 101 | virtual void notify_server_did_request_alert(Badge, String const& message) override; 102 | virtual void notify_server_did_request_confirm(Badge, String const& message) override; 103 | virtual void notify_server_did_request_prompt(Badge, String const& message, String const& default_) override; 104 | virtual void notify_server_did_request_set_prompt_text(Badge, String const& message) override; 105 | virtual void notify_server_did_request_accept_dialog(Badge) override; 106 | virtual void notify_server_did_request_dismiss_dialog(Badge) override; 107 | virtual void notify_server_did_request_file(Badge, DeprecatedString const& path, i32) override; 108 | virtual void notify_server_did_finish_handling_input_event(bool event_was_accepted) override; 109 | 110 | //signals: 111 | // void urls_dropped(QList const&); 112 | 113 | private: 114 | // ^WebView::ViewImplementation 115 | virtual void create_client(WebView::EnableCallgrindProfiling = WebView::EnableCallgrindProfiling::No, WebView::UseJavaScriptBytecode = WebView::UseJavaScriptBytecode::No) override; 116 | virtual void update_zoom() override; 117 | virtual Gfx::IntRect viewport_rect() const override; 118 | virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override; 119 | virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override; 120 | 121 | // bool on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state); 122 | // void on_key_released(guint keyval, guint keycode, Gdk::ModifierType state); 123 | void on_motion(double x, double y); 124 | 125 | Glib::RefPtr m_key_controller; 126 | Glib::RefPtr m_focus_controller; 127 | Glib::RefPtr m_motion_controller; 128 | Glib::RefPtr m_click_gesture; 129 | 130 | bool on_key_pressed(guint keyval, guint keycode, Gdk::ModifierType state); 131 | void on_key_released(guint keyval, guint keycode, Gdk::ModifierType state); 132 | void on_pressed(int n_press, double x, double y); 133 | void on_release(int n_press, double x, double y); 134 | 135 | // TODO: This seems a bit unsafe, we should probably make these optional? 136 | std::shared_ptr m_vertical_adj; 137 | std::shared_ptr m_horizontal_adj; 138 | Gtk::DrawingArea m_drawing_area; 139 | 140 | void update_viewport_rect(); 141 | 142 | float m_inverse_pixel_scaling_ratio { 1.0 }; 143 | bool m_should_show_line_box_borders { false }; 144 | 145 | Glib::RefPtr m_dialog; 146 | 147 | Gfx::IntRect m_viewport_rect; 148 | 149 | StringView m_webdriver_content_ipc_path; 150 | }; 151 | -------------------------------------------------------------------------------- /src/WebDriver/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(WEBDRIVER_SOURCE_DIR ${SERENITY_SOURCE_DIR}/Userland/Services/WebDriver) 2 | 3 | set(SOURCES 4 | ${WEBDRIVER_SOURCE_DIR}/Client.cpp 5 | ${WEBDRIVER_SOURCE_DIR}/Session.cpp 6 | ${WEBDRIVER_SOURCE_DIR}/WebContentConnection.cpp 7 | ../Utilities.cpp 8 | ../HelperProcess.cpp 9 | main.cpp 10 | ) 11 | 12 | add_executable(WebDriver ${SOURCES}) 13 | 14 | target_include_directories(WebDriver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/..) 15 | target_include_directories(WebDriver PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/..) 16 | target_include_directories(WebDriver PRIVATE ${SERENITY_SOURCE_DIR}/Userland) 17 | target_include_directories(WebDriver PRIVATE ${SERENITY_SOURCE_DIR}/Userland/Services) 18 | target_link_libraries(WebDriver PRIVATE ${GTK4_LIBRARIES} LibCore LibFileSystem LibGfx LibIPC LibJS LibMain LibWeb LibWebSocket) 19 | #add_dependencies(WebDriver headless-browser) 20 | -------------------------------------------------------------------------------- /src/WebDriver/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Tim Flynn 3 | * 4 | * SPDX-License-Identifier: BSD-2-Clause 5 | */ 6 | 7 | #include "../HelperProcess.h" 8 | #include "../Utilities.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | extern DeprecatedString s_serenity_resource_root; 22 | 23 | static ErrorOr launch_process(StringView application, ReadonlySpan arguments) 24 | { 25 | auto paths = TRY(get_paths_for_helper_process(application)); 26 | 27 | ErrorOr result = -1; 28 | for (auto const& path : paths) { 29 | auto path_view = path.bytes_as_string_view(); 30 | result = Core::Process::spawn(path_view, arguments, {}, Core::Process::KeepAsChild::Yes); 31 | if (!result.is_error()) 32 | break; 33 | } 34 | return result; 35 | } 36 | 37 | static ErrorOr launch_browser(DeprecatedString const& socket_path) 38 | { 39 | return launch_process("ladybird"sv, 40 | Array { 41 | "--webdriver-content-path", 42 | socket_path.characters(), 43 | }); 44 | } 45 | 46 | static ErrorOr launch_headless_browser(DeprecatedString const& socket_path) 47 | { 48 | auto resources = DeprecatedString::formatted("{}/res", s_serenity_resource_root); 49 | return launch_process("headless-browser"sv, 50 | Array { 51 | "--resources", 52 | resources.characters(), 53 | "--webdriver-ipc-path", 54 | socket_path.characters(), 55 | "about:blank", 56 | }); 57 | } 58 | 59 | ErrorOr serenity_main(Main::Arguments arguments) 60 | { 61 | // Note: only creating this to get access to its static methods in HelperProcess 62 | // QCoreApplication application(arguments.argc, arguments.argv); 63 | Gtk::Application::create("com.mattjakeman.LibWebGTK.WebDriver"); 64 | 65 | auto listen_address = "0.0.0.0"sv; 66 | int port = 8000; 67 | 68 | Core::ArgsParser args_parser; 69 | args_parser.add_option(listen_address, "IP address to listen on", "listen-address", 'l', "listen_address"); 70 | args_parser.add_option(port, "Port to listen on", "port", 'p', "port"); 71 | args_parser.parse(arguments); 72 | 73 | auto ipv4_address = IPv4Address::from_string(listen_address); 74 | if (!ipv4_address.has_value()) { 75 | warnln("Invalid listen address: {}", listen_address); 76 | return 1; 77 | } 78 | 79 | if ((u16)port != port) { 80 | warnln("Invalid port number: {}", port); 81 | return 1; 82 | } 83 | 84 | platform_init(); 85 | 86 | auto webdriver_socket_path = DeprecatedString::formatted("{}/webdriver", TRY(Core::StandardPaths::runtime_directory())); 87 | TRY(Core::Directory::create(webdriver_socket_path, Core::Directory::CreateDirectories::Yes)); 88 | 89 | Core::EventLoop loop; 90 | auto server = TRY(Core::TCPServer::try_create()); 91 | 92 | // FIXME: Propagate errors 93 | server->on_ready_to_accept = [&] { 94 | auto maybe_client_socket = server->accept(); 95 | if (maybe_client_socket.is_error()) { 96 | warnln("Failed to accept the client: {}", maybe_client_socket.error()); 97 | return; 98 | } 99 | 100 | auto maybe_buffered_socket = Core::BufferedTCPSocket::create(maybe_client_socket.release_value()); 101 | if (maybe_buffered_socket.is_error()) { 102 | warnln("Could not obtain a buffered socket for the client: {}", maybe_buffered_socket.error()); 103 | return; 104 | } 105 | 106 | auto maybe_client = WebDriver::Client::try_create(maybe_buffered_socket.release_value(), { launch_browser, launch_headless_browser }, server); 107 | if (maybe_client.is_error()) { 108 | warnln("Could not create a WebDriver client: {}", maybe_client.error()); 109 | return; 110 | } 111 | }; 112 | 113 | TRY(server->listen(ipv4_address.value(), port, Core::TCPServer::AllowAddressReuse::Yes)); 114 | outln("Listening on {}:{}", ipv4_address.value(), port); 115 | 116 | return loop.exec(); 117 | } 118 | -------------------------------------------------------------------------------- /src/WebSocketClientManagerLadybird.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Dex♪ 3 | * Copyright (c) 2022, Andreas Kling 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include "WebSocketClientManagerLadybird.h" 9 | #include "WebSocketImplQt.h" 10 | #include "WebSocketLadybird.h" 11 | 12 | namespace Ladybird { 13 | 14 | NonnullRefPtr WebSocketClientManagerLadybird::create() 15 | { 16 | return adopt_ref(*new WebSocketClientManagerLadybird()); 17 | } 18 | 19 | WebSocketClientManagerLadybird::WebSocketClientManagerLadybird() = default; 20 | WebSocketClientManagerLadybird::~WebSocketClientManagerLadybird() = default; 21 | 22 | RefPtr WebSocketClientManagerLadybird::connect(AK::URL const& url, DeprecatedString const& origin, Vector const& protocols) 23 | { 24 | WebSocket::ConnectionInfo connection_info(url); 25 | connection_info.set_origin(origin); 26 | connection_info.set_protocols(protocols); 27 | 28 | auto impl = adopt_ref(*new WebSocketImplQt); 29 | auto web_socket = WebSocket::WebSocket::create(move(connection_info), move(impl)); 30 | web_socket->start(); 31 | return WebSocketLadybird::create(web_socket); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/WebSocketClientManagerLadybird.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Dex♪ 3 | * Copyright (c) 2022, Andreas Kling 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #pragma once 14 | 15 | namespace Ladybird { 16 | 17 | class WebSocketClientManagerLadybird : public Web::WebSockets::WebSocketClientManager { 18 | public: 19 | static NonnullRefPtr create(); 20 | 21 | virtual ~WebSocketClientManagerLadybird() override; 22 | virtual RefPtr connect(AK::URL const&, DeprecatedString const& origin, Vector const& protocols) override; 23 | 24 | private: 25 | WebSocketClientManagerLadybird(); 26 | }; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/WebSocketImplQt.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Dex♪ 3 | * Copyright (c) 2022, Ali Mohammad Pur 4 | * Copyright (c) 2022, the SerenityOS developers. 5 | * Copyright (c) 2022, Andreas Kling 6 | * 7 | * SPDX-License-Identifier: BSD-2-Clause 8 | */ 9 | 10 | #include "WebSocketImplQt.h" 11 | #include "Utilities.h" 12 | #include 13 | #include 14 | #include 15 | 16 | namespace Ladybird { 17 | 18 | WebSocketImplQt::~WebSocketImplQt() = default; 19 | WebSocketImplQt::WebSocketImplQt() = default; 20 | 21 | bool WebSocketImplQt::can_read_line() 22 | { 23 | return m_socket->canReadLine(); 24 | } 25 | 26 | bool WebSocketImplQt::send(ReadonlyBytes bytes) 27 | { 28 | auto bytes_written = m_socket->write(reinterpret_cast(bytes.data()), bytes.size()); 29 | if (bytes_written == -1) 30 | return false; 31 | VERIFY(static_cast(bytes_written) == bytes.size()); 32 | return true; 33 | } 34 | 35 | bool WebSocketImplQt::eof() 36 | { 37 | return m_socket->state() == QTcpSocket::SocketState::UnconnectedState 38 | && !m_socket->bytesAvailable(); 39 | } 40 | 41 | void WebSocketImplQt::discard_connection() 42 | { 43 | m_socket = nullptr; 44 | } 45 | 46 | void WebSocketImplQt::connect(WebSocket::ConnectionInfo const& connection_info) 47 | { 48 | VERIFY(!m_socket); 49 | VERIFY(on_connected); 50 | VERIFY(on_connection_error); 51 | VERIFY(on_ready_to_read); 52 | 53 | if (connection_info.is_secure()) { 54 | auto ssl_socket = make(); 55 | ssl_socket->connectToHostEncrypted( 56 | qstring_from_ak_deprecated_string(connection_info.url().host()), 57 | connection_info.url().port_or_default()); 58 | QObject::connect(ssl_socket.ptr(), &QSslSocket::alertReceived, [this](QSsl::AlertLevel level, QSsl::AlertType, QString const&) { 59 | if (level == QSsl::AlertLevel::Fatal) 60 | on_connection_error(); 61 | }); 62 | m_socket = move(ssl_socket); 63 | } else { 64 | m_socket = make(); 65 | m_socket->connectToHost( 66 | qstring_from_ak_deprecated_string(connection_info.url().host()), 67 | connection_info.url().port_or_default()); 68 | } 69 | 70 | QObject::connect(m_socket.ptr(), &QTcpSocket::readyRead, [this] { 71 | on_ready_to_read(); 72 | }); 73 | 74 | QObject::connect(m_socket.ptr(), &QTcpSocket::connected, [this] { 75 | on_connected(); 76 | }); 77 | } 78 | 79 | ErrorOr WebSocketImplQt::read(int max_size) 80 | { 81 | auto buffer = TRY(ByteBuffer::create_uninitialized(max_size)); 82 | auto bytes_read = m_socket->read(reinterpret_cast(buffer.data()), buffer.size()); 83 | if (bytes_read == -1) 84 | return Error::from_string_literal("WebSocketImplQt::read(): Error reading from socket"); 85 | return buffer.slice(0, bytes_read); 86 | } 87 | 88 | ErrorOr WebSocketImplQt::read_line(size_t size) 89 | { 90 | auto buffer = TRY(ByteBuffer::create_uninitialized(size)); 91 | auto bytes_read = m_socket->readLine(reinterpret_cast(buffer.data()), buffer.size()); 92 | if (bytes_read == -1) 93 | return Error::from_string_literal("WebSocketImplQt::read_line(): Error reading from socket"); 94 | return DeprecatedString::copy(buffer.span().slice(0, bytes_read)); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/WebSocketImplQt.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Dex♪ 3 | * Copyright (c) 2022, Ali Mohammad Pur 4 | * Copyright (c) 2022, the SerenityOS developers. 5 | * Copyright (c) 2022, Andreas Kling 6 | * 7 | * SPDX-License-Identifier: BSD-2-Clause 8 | */ 9 | 10 | #pragma once 11 | 12 | #include 13 | 14 | class QTcpSocket; 15 | 16 | namespace Ladybird { 17 | 18 | class WebSocketImplQt final : public WebSocket::WebSocketImpl { 19 | public: 20 | explicit WebSocketImplQt(); 21 | virtual ~WebSocketImplQt() override; 22 | 23 | virtual void connect(WebSocket::ConnectionInfo const&) override; 24 | virtual bool can_read_line() override; 25 | virtual ErrorOr read_line(size_t) override; 26 | virtual ErrorOr read(int max_size) override; 27 | virtual bool send(ReadonlyBytes) override; 28 | virtual bool eof() override; 29 | virtual void discard_connection() override; 30 | 31 | private: 32 | OwnPtr m_socket; 33 | }; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/WebSocketLadybird.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Dex♪ 3 | * Copyright (c) 2022, Andreas Kling 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #include "WebSocketLadybird.h" 9 | 10 | namespace Ladybird { 11 | 12 | NonnullRefPtr WebSocketLadybird::create(NonnullRefPtr underlying_socket) 13 | { 14 | return adopt_ref(*new WebSocketLadybird(move(underlying_socket))); 15 | } 16 | 17 | WebSocketLadybird::WebSocketLadybird(NonnullRefPtr underlying_socket) 18 | : m_websocket(move(underlying_socket)) 19 | { 20 | m_websocket->on_open = [weak_this = make_weak_ptr()] { 21 | if (auto strong_this = weak_this.strong_ref()) 22 | if (strong_this->on_open) 23 | strong_this->on_open(); 24 | }; 25 | m_websocket->on_message = [weak_this = make_weak_ptr()](auto message) { 26 | if (auto strong_this = weak_this.strong_ref()) { 27 | if (strong_this->on_message) { 28 | strong_this->on_message(Web::WebSockets::WebSocketClientSocket::Message { 29 | .data = move(message.data()), 30 | .is_text = message.is_text(), 31 | }); 32 | } 33 | } 34 | }; 35 | m_websocket->on_error = [weak_this = make_weak_ptr()](auto error) { 36 | if (auto strong_this = weak_this.strong_ref()) { 37 | if (strong_this->on_error) { 38 | switch (error) { 39 | case WebSocket::WebSocket::Error::CouldNotEstablishConnection: 40 | strong_this->on_error(Web::WebSockets::WebSocketClientSocket::Error::CouldNotEstablishConnection); 41 | return; 42 | case WebSocket::WebSocket::Error::ConnectionUpgradeFailed: 43 | strong_this->on_error(Web::WebSockets::WebSocketClientSocket::Error::ConnectionUpgradeFailed); 44 | return; 45 | case WebSocket::WebSocket::Error::ServerClosedSocket: 46 | strong_this->on_error(Web::WebSockets::WebSocketClientSocket::Error::ServerClosedSocket); 47 | return; 48 | } 49 | VERIFY_NOT_REACHED(); 50 | } 51 | } 52 | }; 53 | m_websocket->on_close = [weak_this = make_weak_ptr()](u16 code, DeprecatedString reason, bool was_clean) { 54 | if (auto strong_this = weak_this.strong_ref()) 55 | if (strong_this->on_close) 56 | strong_this->on_close(code, move(reason), was_clean); 57 | }; 58 | } 59 | 60 | WebSocketLadybird::~WebSocketLadybird() = default; 61 | 62 | Web::WebSockets::WebSocket::ReadyState WebSocketLadybird::ready_state() 63 | { 64 | switch (m_websocket->ready_state()) { 65 | case WebSocket::ReadyState::Connecting: 66 | return Web::WebSockets::WebSocket::ReadyState::Connecting; 67 | case WebSocket::ReadyState::Open: 68 | return Web::WebSockets::WebSocket::ReadyState::Open; 69 | case WebSocket::ReadyState::Closing: 70 | return Web::WebSockets::WebSocket::ReadyState::Closing; 71 | case WebSocket::ReadyState::Closed: 72 | return Web::WebSockets::WebSocket::ReadyState::Closed; 73 | } 74 | VERIFY_NOT_REACHED(); 75 | } 76 | 77 | DeprecatedString WebSocketLadybird::subprotocol_in_use() 78 | { 79 | return m_websocket->subprotocol_in_use(); 80 | } 81 | 82 | void WebSocketLadybird::send(ByteBuffer binary_or_text_message, bool is_text) 83 | { 84 | m_websocket->send(WebSocket::Message(binary_or_text_message, is_text)); 85 | } 86 | 87 | void WebSocketLadybird::send(StringView message) 88 | { 89 | m_websocket->send(WebSocket::Message(message)); 90 | } 91 | 92 | void WebSocketLadybird::close(u16 code, DeprecatedString reason) 93 | { 94 | m_websocket->close(code, reason); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/WebSocketLadybird.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022, Dex♪ 3 | * Copyright (c) 2022, Andreas Kling 4 | * 5 | * SPDX-License-Identifier: BSD-2-Clause 6 | */ 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace Ladybird { 14 | 15 | class WebSocketLadybird 16 | : public Web::WebSockets::WebSocketClientSocket 17 | , public Weakable { 18 | public: 19 | static NonnullRefPtr create(NonnullRefPtr); 20 | 21 | virtual ~WebSocketLadybird() override; 22 | 23 | virtual Web::WebSockets::WebSocket::ReadyState ready_state() override; 24 | virtual DeprecatedString subprotocol_in_use() override; 25 | virtual void send(ByteBuffer binary_or_text_message, bool is_text) override; 26 | virtual void send(StringView message) override; 27 | virtual void close(u16 code, DeprecatedString reason) override; 28 | 29 | private: 30 | explicit WebSocketLadybird(NonnullRefPtr); 31 | 32 | NonnullRefPtr m_websocket; 33 | }; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/config.toml: -------------------------------------------------------------------------------- 1 | [library] 2 | description = "LibWeb GTK" 3 | authors = "Matthew Jakeman" 4 | license = "BSD 2-Clause" 5 | browse_url = "https://github.com/mjakeman/libweb-gtk" 6 | repository_url = "https://github.com/mjakeman/libweb-gtk.git" 7 | website_url = "https://github.com/mjakeman/libweb-gtk" 8 | dependencies = [ 9 | "GObject-2.0", 10 | "Gtk-4.0", 11 | "Soup-3.0" 12 | ] 13 | 14 | [dependencies."GObject-2.0"] 15 | name = "GObject" 16 | description = "The base type system library" 17 | docs_url = "https://docs.gtk.org/gobject/" 18 | 19 | [dependencies."Gtk-4.0"] 20 | name = "Gtk" 21 | description = "The GTK toolkit" 22 | docs_url = "https://docs.gtk.org/gtk4/" 23 | 24 | [dependencies."Soup-3.0"] 25 | name = "Soup" 26 | description = "HTTP client/server library for GNOME" 27 | docs_url = "https://libsoup.org/libsoup-3.0/" 28 | --------------------------------------------------------------------------------