Everywhere: Move the Ladybird folder to UI
Author: https://github.com/trflynn89
Commit: db47cc41f8
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2256
Reviewed-by: https://github.com/sideshowbarker
8
UI/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.qmake.stash
|
||||
Makefile
|
||||
*.o
|
||||
moc_*
|
||||
Build
|
||||
build
|
||||
CMakeLists.txt.user
|
||||
Android/src/main/assets/
|
||||
46
UI/Android/BuildLagomTools.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
LADYBIRD_SOURCE_DIR="$(realpath "${DIR}"/../..)"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "${LADYBIRD_SOURCE_DIR}/Meta/shell_include.sh"
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "${LADYBIRD_SOURCE_DIR}/Meta/find_compiler.sh"
|
||||
|
||||
pick_host_compiler
|
||||
|
||||
BUILD_DIR=${BUILD_DIR:-"${LADYBIRD_SOURCE_DIR}/Build"}
|
||||
CACHE_DIR=${CACHE_DIR:-"${BUILD_DIR}/caches"}
|
||||
|
||||
# HACK: This export of XDG_CACHE_HOME is required to make vcpkg happy.
|
||||
# This is because vcpkg tries to find a cache directory by:
|
||||
# 1) checking $XDG_CACHE_HOME
|
||||
# 2) appending "/.cache" to $HOME
|
||||
# The problem is, in the Android build environment, neither of those environment variables are set.
|
||||
# This causes vcpkg to fail; so, we set a dummy $XDG_CACHE_HOME, ensuring that vcpkg is happy.
|
||||
# (Note that vcpkg appends "/vcpkg" to the cache directory we give it.)
|
||||
# (And this also works on macOS, despite the fact that $XDG_CACHE_HOME is a Linux-ism.)
|
||||
export XDG_CACHE_HOME="$CACHE_DIR"
|
||||
|
||||
"$LADYBIRD_SOURCE_DIR"/Meta/ladybird.sh vcpkg
|
||||
|
||||
cmake -S "${LADYBIRD_SOURCE_DIR}/Meta/Lagom" -B "$BUILD_DIR/lagom-tools" \
|
||||
-GNinja -Dpackage=LagomTools \
|
||||
-DCMAKE_INSTALL_PREFIX="$BUILD_DIR/lagom-tools-install" \
|
||||
-DCMAKE_C_COMPILER="$CC" \
|
||||
-DCMAKE_CXX_COMPILER="$CXX" \
|
||||
-DSERENITY_CACHE_DIR="$CACHE_DIR" \
|
||||
-DLAGOM_TOOLS_ONLY=ON \
|
||||
-DINSTALL_LAGOM_TOOLS=ON \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$LADYBIRD_SOURCE_DIR/Build/vcpkg/scripts/buildsystems/vcpkg.cmake" \
|
||||
-DVCPKG_INSTALL_OPTIONS="--no-print-usage" \
|
||||
-DVCPKG_OVERLAY_TRIPLETS="$LADYBIRD_SOURCE_DIR/Meta/CMake/vcpkg/release-triplets" \
|
||||
-DVCPKG_ROOT="$LADYBIRD_SOURCE_DIR/Build/vcpkg" \
|
||||
-DVCPKG_MANIFEST_DIR="$LADYBIRD_SOURCE_DIR"
|
||||
|
||||
ninja -C "$BUILD_DIR/lagom-tools" install
|
||||
13
UI/Android/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
add_library(ladybird SHARED
|
||||
${LADYBIRD_SOURCES}
|
||||
src/main/cpp/LadybirdActivity.cpp
|
||||
src/main/cpp/WebViewImplementationNative.cpp
|
||||
src/main/cpp/WebViewImplementationNativeJNI.cpp
|
||||
src/main/cpp/ALooperEventLoopImplementation.cpp
|
||||
src/main/cpp/TimerExecutorService.cpp
|
||||
src/main/cpp/JNIHelpers.cpp
|
||||
)
|
||||
target_link_libraries(ladybird PRIVATE LibArchive jnigraphics android)
|
||||
|
||||
include(../cmake/AndroidExtras.cmake)
|
||||
create_ladybird_bundle(ladybird)
|
||||
95
UI/Android/build.gradle.kts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
|
||||
plugins {
|
||||
id("com.android.application") version "8.4.0"
|
||||
id("org.jetbrains.kotlin.android") version "1.9.0"
|
||||
}
|
||||
|
||||
var buildDir = layout.buildDirectory.get()
|
||||
var cacheDir = System.getenv("SERENITY_CACHE_DIR") ?: "$buildDir/caches"
|
||||
var sourceDir = layout.projectDirectory.dir("../../").toString()
|
||||
|
||||
task<Exec>("buildLagomTools") {
|
||||
commandLine = listOf("./BuildLagomTools.sh")
|
||||
environment = mapOf(
|
||||
"BUILD_DIR" to buildDir,
|
||||
"CACHE_DIR" to cacheDir,
|
||||
"PATH" to System.getenv("PATH")!!
|
||||
)
|
||||
}
|
||||
tasks.named("preBuild").dependsOn("buildLagomTools")
|
||||
tasks.named("prepareKotlinBuildScriptModel").dependsOn("buildLagomTools")
|
||||
|
||||
android {
|
||||
namespace = "org.serenityos.ladybird"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.serenityos.ladybird"
|
||||
minSdk = 30
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
// FIXME: Use -std=c++23 once the Android NDK's clang supports that.
|
||||
cppFlags += "-std=c++2b"
|
||||
arguments += listOf(
|
||||
"-DLagomTools_DIR=$buildDir/lagom-tools-install/share/LagomTools",
|
||||
"-DANDROID_STL=c++_shared",
|
||||
"-DSERENITY_CACHE_DIR=$cacheDir",
|
||||
"-DVCPKG_ROOT=$sourceDir/Build/vcpkg",
|
||||
"-DVCPKG_TARGET_ANDROID=ON"
|
||||
)
|
||||
}
|
||||
}
|
||||
ndk {
|
||||
// Specifies the ABI configurations of your native
|
||||
// libraries Gradle should build and package with your app.
|
||||
abiFilters += listOf("x86_64", "arm64-v8a")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("../../CMakeLists.txt")
|
||||
version = "3.23.0+"
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
prefab = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
implementation("com.google.oboe:oboe:1.9.0")
|
||||
}
|
||||
23
UI/Android/gradle.properties
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Project-wide Gradle settings.
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
BIN
UI/Android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
UI/Android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#Fri Sep 01 12:36:55 CEST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
185
UI/Android/gradlew
vendored
Executable file
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
21
UI/Android/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
16
UI/Android/settings.gradle.kts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "Ladybird"
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package org.serenityos.ladybird
|
||||
|
||||
import androidx.test.ext.junit.rules.activityScenarioRule
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Rule
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmokeTest {
|
||||
|
||||
@get:Rule
|
||||
var activityScenarioRule = activityScenarioRule<LadybirdActivity>()
|
||||
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("org.serenityos.ladybird", appContext.packageName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun loadWebView() {
|
||||
// We can actually load a web view, and it is visible
|
||||
onView(withId(R.id.web_view)).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
||||
66
UI/Android/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto"
|
||||
android:versionCode="001"
|
||||
android:versionName="head">
|
||||
|
||||
<supports-screens
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true" />
|
||||
|
||||
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS" />
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:allowNativeHeapPointerTagging="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:fullBackupOnly="false"
|
||||
android:hardwareAccelerated="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Ladybird"
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name=".LadybirdActivity"
|
||||
android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="unspecified">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.extract_android_style"
|
||||
android:value="minimal" />
|
||||
</activity>
|
||||
<service
|
||||
android:name=".WebContentService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":WebContent" />
|
||||
<service
|
||||
android:name=".RequestServerService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":RequestServer" />
|
||||
<service
|
||||
android:name=".ImageDecoderService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:process=":ImageDecoder" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
262
UI/Android/src/main/cpp/ALooperEventLoopImplementation.cpp
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ALooperEventLoopImplementation.h"
|
||||
#include "JNIHelpers.h"
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <android/log.h>
|
||||
#include <android/looper.h>
|
||||
#include <fcntl.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
EventLoopThreadData& EventLoopThreadData::the()
|
||||
{
|
||||
static thread_local EventLoopThreadData s_thread_data { {}, {}, &Core::ThreadEventQueue::current() };
|
||||
return s_thread_data;
|
||||
}
|
||||
|
||||
static ALooperEventLoopImplementation& current_impl()
|
||||
{
|
||||
return verify_cast<ALooperEventLoopImplementation>(Core::EventLoop::current().impl());
|
||||
}
|
||||
|
||||
static int looper_callback(int fd, int events, void* data);
|
||||
|
||||
ALooperEventLoopManager::ALooperEventLoopManager(jobject timer_service)
|
||||
: m_timer_service(timer_service)
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
|
||||
jclass timer_class = env.get()->FindClass("org/serenityos/ladybird/TimerExecutorService$Timer");
|
||||
if (!timer_class)
|
||||
TODO();
|
||||
m_timer_class = reinterpret_cast<jclass>(env.get()->NewGlobalRef(timer_class));
|
||||
env.get()->DeleteLocalRef(timer_class);
|
||||
|
||||
m_timer_constructor = env.get()->GetMethodID(m_timer_class, "<init>", "(J)V");
|
||||
if (!m_timer_constructor)
|
||||
TODO();
|
||||
|
||||
jclass timer_service_class = env.get()->GetObjectClass(m_timer_service);
|
||||
|
||||
m_register_timer = env.get()->GetMethodID(timer_service_class, "registerTimer", "(Lorg/serenityos/ladybird/TimerExecutorService$Timer;ZJ)J");
|
||||
if (!m_register_timer)
|
||||
TODO();
|
||||
|
||||
m_unregister_timer = env.get()->GetMethodID(timer_service_class, "unregisterTimer", "(J)V");
|
||||
if (!m_unregister_timer)
|
||||
TODO();
|
||||
env.get()->DeleteLocalRef(timer_service_class);
|
||||
|
||||
auto ret = pipe2(m_pipe, O_CLOEXEC | O_NONBLOCK);
|
||||
VERIFY(ret == 0);
|
||||
|
||||
m_main_looper = ALooper_forThread();
|
||||
VERIFY(m_main_looper);
|
||||
ALooper_acquire(m_main_looper);
|
||||
|
||||
ret = ALooper_addFd(m_main_looper, m_pipe[0], ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, &looper_callback, this);
|
||||
VERIFY(ret == 1);
|
||||
}
|
||||
|
||||
ALooperEventLoopManager::~ALooperEventLoopManager()
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
|
||||
env.get()->DeleteGlobalRef(m_timer_service);
|
||||
env.get()->DeleteGlobalRef(m_timer_class);
|
||||
|
||||
ALooper_removeFd(m_main_looper, m_pipe[0]);
|
||||
ALooper_release(m_main_looper);
|
||||
|
||||
::close(m_pipe[0]);
|
||||
::close(m_pipe[1]);
|
||||
}
|
||||
|
||||
NonnullOwnPtr<Core::EventLoopImplementation> ALooperEventLoopManager::make_implementation()
|
||||
{
|
||||
return ALooperEventLoopImplementation::create();
|
||||
}
|
||||
|
||||
intptr_t ALooperEventLoopManager::register_timer(Core::EventReceiver& receiver, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible visibility)
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
auto& thread_data = EventLoopThreadData::the();
|
||||
|
||||
auto timer = env.get()->NewObject(m_timer_class, m_timer_constructor, reinterpret_cast<long>(¤t_impl()));
|
||||
|
||||
long millis = milliseconds;
|
||||
long timer_id = env.get()->CallLongMethod(m_timer_service, m_register_timer, timer, !should_reload, millis);
|
||||
|
||||
// FIXME: Is there a race condition here? Maybe we should take a lock on the timers...
|
||||
thread_data.timers.set(timer_id, { receiver.make_weak_ptr(), visibility });
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::unregister_timer(intptr_t timer_id)
|
||||
{
|
||||
if (auto timer = EventLoopThreadData::the().timers.take(timer_id); timer.has_value()) {
|
||||
JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(m_timer_service, m_unregister_timer, timer_id);
|
||||
}
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
EventLoopThreadData::the().notifiers.set(¬ifier);
|
||||
current_impl().register_notifier(notifier);
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
EventLoopThreadData::the().notifiers.remove(¬ifier);
|
||||
current_impl().unregister_notifier(notifier);
|
||||
}
|
||||
|
||||
void ALooperEventLoopManager::did_post_event()
|
||||
{
|
||||
int msg = 0xCAFEBABE;
|
||||
(void)write(m_pipe[1], &msg, sizeof(msg));
|
||||
}
|
||||
|
||||
int looper_callback(int fd, int events, void* data)
|
||||
{
|
||||
auto& manager = *static_cast<ALooperEventLoopManager*>(data);
|
||||
|
||||
if (events & ALOOPER_EVENT_INPUT) {
|
||||
int msg = 0;
|
||||
while (read(fd, &msg, sizeof(msg)) == sizeof(msg)) {
|
||||
// Do nothing, we don't actually care what the message was, just that it was posted
|
||||
}
|
||||
manager.on_did_post_event();
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
ALooperEventLoopImplementation::ALooperEventLoopImplementation()
|
||||
: m_event_loop(ALooper_prepare(0))
|
||||
, m_thread_data(&EventLoopThreadData::the())
|
||||
{
|
||||
ALooper_acquire(m_event_loop);
|
||||
}
|
||||
|
||||
ALooperEventLoopImplementation::~ALooperEventLoopImplementation()
|
||||
{
|
||||
ALooper_release(m_event_loop);
|
||||
}
|
||||
|
||||
EventLoopThreadData& ALooperEventLoopImplementation::thread_data()
|
||||
{
|
||||
return *m_thread_data;
|
||||
}
|
||||
|
||||
int ALooperEventLoopImplementation::exec()
|
||||
{
|
||||
while (!m_exit_requested.load(MemoryOrder::memory_order_acquire))
|
||||
pump(PumpMode::WaitForEvents);
|
||||
return m_exit_code;
|
||||
}
|
||||
|
||||
size_t ALooperEventLoopImplementation::pump(Core::EventLoopImplementation::PumpMode mode)
|
||||
{
|
||||
auto num_events = Core::ThreadEventQueue::current().process();
|
||||
|
||||
int timeout_ms = mode == Core::EventLoopImplementation::PumpMode::WaitForEvents ? -1 : 0;
|
||||
int ret;
|
||||
do {
|
||||
ret = ALooper_pollOnce(timeout_ms, nullptr, nullptr, nullptr);
|
||||
} while (ret == ALOOPER_POLL_CALLBACK);
|
||||
|
||||
// We don't expect any non-callback FDs to be ready
|
||||
VERIFY(ret <= 0);
|
||||
|
||||
if (ret == ALOOPER_POLL_ERROR)
|
||||
m_exit_requested.store(true, MemoryOrder::memory_order_release);
|
||||
|
||||
num_events += Core::ThreadEventQueue::current().process();
|
||||
return num_events;
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::quit(int code)
|
||||
{
|
||||
m_exit_code = code;
|
||||
m_exit_requested.store(true, MemoryOrder::memory_order_release);
|
||||
wake();
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::wake()
|
||||
{
|
||||
ALooper_wake(m_event_loop);
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&& event)
|
||||
{
|
||||
m_thread_event_queue.post_event(receiver, move(event));
|
||||
|
||||
if (&m_thread_event_queue != &Core::ThreadEventQueue::current())
|
||||
wake();
|
||||
}
|
||||
|
||||
static int notifier_callback(int fd, int events, void* data)
|
||||
{
|
||||
auto& notifier = *static_cast<Core::Notifier*>(data);
|
||||
|
||||
VERIFY(fd == notifier.fd());
|
||||
|
||||
Core::NotificationType type = Core::NotificationType::None;
|
||||
if (events & ALOOPER_EVENT_INPUT)
|
||||
type |= Core::NotificationType::Read;
|
||||
if (events & ALOOPER_EVENT_OUTPUT)
|
||||
type |= Core::NotificationType::Write;
|
||||
if (events & ALOOPER_EVENT_HANGUP)
|
||||
type |= Core::NotificationType::HangUp;
|
||||
if (events & ALOOPER_EVENT_ERROR)
|
||||
type |= Core::NotificationType::Error;
|
||||
|
||||
Core::NotifierActivationEvent event(notifier.fd(), type);
|
||||
notifier.dispatch_event(event);
|
||||
|
||||
// Wake up from ALooper_pollAll, and service this event on the event queue
|
||||
current_impl().wake();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
auto event_flags = 0;
|
||||
switch (notifier.type()) {
|
||||
case Core::Notifier::Type::Read:
|
||||
event_flags = ALOOPER_EVENT_INPUT;
|
||||
break;
|
||||
case Core::Notifier::Type::Write:
|
||||
event_flags = ALOOPER_EVENT_OUTPUT;
|
||||
break;
|
||||
case Core::Notifier::Type::Error:
|
||||
event_flags = ALOOPER_EVENT_ERROR;
|
||||
break;
|
||||
case Core::Notifier::Type::HangUp:
|
||||
event_flags = ALOOPER_EVENT_HANGUP;
|
||||
break;
|
||||
case Core::Notifier::Type::None:
|
||||
TODO();
|
||||
}
|
||||
|
||||
auto ret = ALooper_addFd(m_event_loop, notifier.fd(), ALOOPER_POLL_CALLBACK, event_flags, ¬ifier_callback, ¬ifier);
|
||||
VERIFY(ret == 1);
|
||||
}
|
||||
|
||||
void ALooperEventLoopImplementation::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
ALooper_removeFd(m_event_loop, notifier.fd());
|
||||
}
|
||||
|
||||
}
|
||||
96
UI/Android/src/main/cpp/ALooperEventLoopImplementation.h
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Atomic.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibCore/EventLoopImplementation.h>
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" struct ALooper;
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class ALooperEventLoopManager : public Core::EventLoopManager {
|
||||
public:
|
||||
ALooperEventLoopManager(jobject timer_service);
|
||||
virtual ~ALooperEventLoopManager() override;
|
||||
virtual NonnullOwnPtr<Core::EventLoopImplementation> make_implementation() override;
|
||||
|
||||
virtual intptr_t register_timer(Core::EventReceiver&, int milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override;
|
||||
virtual void unregister_timer(intptr_t timer_id) override;
|
||||
|
||||
virtual void register_notifier(Core::Notifier&) override;
|
||||
virtual void unregister_notifier(Core::Notifier&) override;
|
||||
|
||||
virtual void did_post_event() override;
|
||||
|
||||
Function<void()> on_did_post_event;
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual int register_signal(int, Function<void(int)>) override { return 0; }
|
||||
virtual void unregister_signal(int) override { }
|
||||
|
||||
private:
|
||||
int m_pipe[2] = {};
|
||||
ALooper* m_main_looper { nullptr };
|
||||
jobject m_timer_service { nullptr };
|
||||
jmethodID m_register_timer { nullptr };
|
||||
jmethodID m_unregister_timer { nullptr };
|
||||
jclass m_timer_class { nullptr };
|
||||
jmethodID m_timer_constructor { nullptr };
|
||||
};
|
||||
|
||||
struct TimerData {
|
||||
WeakPtr<Core::EventReceiver> receiver;
|
||||
Core::TimerShouldFireWhenNotVisible visibility;
|
||||
};
|
||||
|
||||
struct EventLoopThreadData {
|
||||
static EventLoopThreadData& the();
|
||||
|
||||
HashMap<long, TimerData> timers;
|
||||
HashTable<Core::Notifier*> notifiers;
|
||||
Core::ThreadEventQueue* thread_queue = nullptr;
|
||||
};
|
||||
|
||||
class ALooperEventLoopImplementation : public Core::EventLoopImplementation {
|
||||
public:
|
||||
static NonnullOwnPtr<ALooperEventLoopImplementation> create() { return adopt_own(*new ALooperEventLoopImplementation); }
|
||||
|
||||
virtual ~ALooperEventLoopImplementation() override;
|
||||
|
||||
virtual int exec() override;
|
||||
virtual size_t pump(PumpMode) override;
|
||||
virtual void quit(int) override;
|
||||
virtual void wake() override;
|
||||
virtual void post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&&) override;
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual void unquit() override { }
|
||||
virtual bool was_exit_requested() const override { return false; }
|
||||
virtual void notify_forked_and_in_child() override { }
|
||||
|
||||
EventLoopThreadData& thread_data();
|
||||
|
||||
private:
|
||||
friend class ALooperEventLoopManager;
|
||||
|
||||
ALooperEventLoopImplementation();
|
||||
|
||||
void register_notifier(Core::Notifier&);
|
||||
void unregister_notifier(Core::Notifier&);
|
||||
|
||||
ALooper* m_event_loop { nullptr };
|
||||
int m_exit_code { 0 };
|
||||
Atomic<bool> m_exit_requested { false };
|
||||
EventLoopThreadData* m_thread_data { nullptr };
|
||||
};
|
||||
|
||||
}
|
||||
22
UI/Android/src/main/cpp/ImageDecoderService.cpp
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
* Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <ImageDecoder/ConnectionFromClient.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibIPC/SingleServer.h>
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket)
|
||||
{
|
||||
Core::EventLoop event_loop;
|
||||
|
||||
auto socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
|
||||
auto client = TRY(ImageDecoder::ConnectionFromClient::try_create(move(socket)));
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
||||
16
UI/Android/src/main/cpp/JNIHelpers.cpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "JNIHelpers.h"
|
||||
#include <AK/Utf16View.h>
|
||||
|
||||
namespace Ladybird {
|
||||
jstring JavaEnvironment::jstring_from_ak_string(String const& str)
|
||||
{
|
||||
auto as_utf16 = MUST(AK::utf8_to_utf16(str.code_points()));
|
||||
return m_env->NewString(as_utf16.data(), as_utf16.size());
|
||||
}
|
||||
}
|
||||
49
UI/Android/src/main/cpp/JNIHelpers.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/String.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
class JavaEnvironment {
|
||||
public:
|
||||
JavaEnvironment(JavaVM* vm)
|
||||
: m_vm(vm)
|
||||
{
|
||||
auto ret = m_vm->GetEnv(reinterpret_cast<void**>(&m_env), JNI_VERSION_1_6);
|
||||
if (ret == JNI_EDETACHED) {
|
||||
ret = m_vm->AttachCurrentThread(&m_env, nullptr);
|
||||
VERIFY(ret == JNI_OK);
|
||||
m_did_attach_thread = true;
|
||||
} else if (ret == JNI_EVERSION) {
|
||||
VERIFY_NOT_REACHED();
|
||||
} else {
|
||||
VERIFY(ret == JNI_OK);
|
||||
}
|
||||
|
||||
VERIFY(m_env != nullptr);
|
||||
}
|
||||
|
||||
~JavaEnvironment()
|
||||
{
|
||||
if (m_did_attach_thread)
|
||||
m_vm->DetachCurrentThread();
|
||||
}
|
||||
|
||||
JNIEnv* get() const { return m_env; }
|
||||
|
||||
jstring jstring_from_ak_string(String const& str);
|
||||
|
||||
private:
|
||||
JavaVM* m_vm = nullptr;
|
||||
JNIEnv* m_env = nullptr;
|
||||
bool m_did_attach_thread = false;
|
||||
};
|
||||
}
|
||||
extern JavaVM* global_vm;
|
||||
258
UI/Android/src/main/cpp/LadybirdActivity.cpp
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ALooperEventLoopImplementation.h"
|
||||
#include "JNIHelpers.h"
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Format.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibArchive/TarStream.h>
|
||||
#include <LibCore/DirIterator.h>
|
||||
#include <LibCore/Directory.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <jni.h>
|
||||
|
||||
static ErrorOr<void> extract_tar_archive(String archive_file, ByteString output_directory);
|
||||
|
||||
JavaVM* global_vm;
|
||||
static OwnPtr<WebView::Application> s_application;
|
||||
static OwnPtr<Core::EventLoop> s_main_event_loop;
|
||||
static jobject s_java_instance;
|
||||
static jmethodID s_schedule_event_loop_method;
|
||||
|
||||
struct Application : public WebView::Application {
|
||||
WEB_VIEW_APPLICATION(Application);
|
||||
};
|
||||
|
||||
Application::Application(Badge<WebView::Application>, Main::Arguments&)
|
||||
{
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv*, jobject, jstring, jstring, jobject);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_initNativeCode(JNIEnv* env, jobject thiz, jstring resource_dir, jstring tag_name, jobject timer_service)
|
||||
{
|
||||
char const* raw_resource_dir = env->GetStringUTFChars(resource_dir, nullptr);
|
||||
s_ladybird_resource_root = raw_resource_dir;
|
||||
env->ReleaseStringUTFChars(resource_dir, raw_resource_dir);
|
||||
|
||||
char const* raw_tag_name = env->GetStringUTFChars(tag_name, nullptr);
|
||||
AK::set_log_tag_name(raw_tag_name);
|
||||
env->ReleaseStringUTFChars(tag_name, raw_tag_name);
|
||||
|
||||
dbgln("Set resource dir to {}", s_ladybird_resource_root);
|
||||
|
||||
auto file_or_error = Core::System::open(MUST(String::formatted("{}/res/icons/48x48/app-browser.png", s_ladybird_resource_root)), O_RDONLY);
|
||||
if (file_or_error.is_error()) {
|
||||
dbgln("No resource files, extracting assets...");
|
||||
MUST(extract_tar_archive(MUST(String::formatted("{}/ladybird-assets.tar", s_ladybird_resource_root)), s_ladybird_resource_root));
|
||||
} else {
|
||||
dbgln("Found app-browser.png, not re-extracting assets.");
|
||||
dbgln("Hopefully no developer changed the asset files and expected them to be re-extracted!");
|
||||
}
|
||||
|
||||
env->GetJavaVM(&global_vm);
|
||||
VERIFY(global_vm);
|
||||
|
||||
s_java_instance = env->NewGlobalRef(thiz);
|
||||
jclass clazz = env->GetObjectClass(s_java_instance);
|
||||
VERIFY(clazz);
|
||||
s_schedule_event_loop_method = env->GetMethodID(clazz, "scheduleEventLoop", "()V");
|
||||
VERIFY(s_schedule_event_loop_method);
|
||||
env->DeleteLocalRef(clazz);
|
||||
|
||||
jobject timer_service_ref = env->NewGlobalRef(timer_service);
|
||||
|
||||
auto* event_loop_manager = new Ladybird::ALooperEventLoopManager(timer_service_ref);
|
||||
event_loop_manager->on_did_post_event = [] {
|
||||
Ladybird::JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(s_java_instance, s_schedule_event_loop_method);
|
||||
};
|
||||
Core::EventLoopManager::install(*event_loop_manager);
|
||||
s_main_event_loop = make<Core::EventLoop>();
|
||||
|
||||
// The strings cannot be empty
|
||||
Main::Arguments arguments = {
|
||||
.argc = 0,
|
||||
.argv = nullptr,
|
||||
.strings = Span<StringView> { new StringView("ladybird"sv), 1 }
|
||||
};
|
||||
|
||||
// FIXME: We are not making use of this Application object to track our processes.
|
||||
// So, right now, the Application's ProcessManager is constantly empty.
|
||||
// (However, LibWebView depends on an Application object existing, so we do have to actually create one.)
|
||||
s_application = Application::create(arguments, "about:newtab"sv);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_execMainEventLoop(JNIEnv*, jobject /* thiz */);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_execMainEventLoop(JNIEnv*, jobject /* thiz */)
|
||||
{
|
||||
if (s_main_event_loop) {
|
||||
s_main_event_loop->pump(Core::EventLoop::WaitMode::PollForEvents);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_disposeNativeCode(JNIEnv*, jobject /* thiz */);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdActivity_disposeNativeCode(JNIEnv* env, jobject /* thiz */)
|
||||
{
|
||||
s_main_event_loop = nullptr;
|
||||
s_schedule_event_loop_method = nullptr;
|
||||
s_application = nullptr;
|
||||
env->DeleteGlobalRef(s_java_instance);
|
||||
|
||||
delete &Core::EventLoopManager::the();
|
||||
}
|
||||
|
||||
ErrorOr<void> extract_tar_archive(String archive_file, ByteString output_directory)
|
||||
{
|
||||
constexpr size_t buffer_size = 4096;
|
||||
|
||||
auto file = TRY(Core::InputBufferedFile::create(TRY(Core::File::open(archive_file, Core::File::OpenMode::Read))));
|
||||
|
||||
ByteString old_pwd = TRY(Core::System::getcwd());
|
||||
|
||||
TRY(Core::System::chdir(output_directory));
|
||||
ScopeGuard go_back = [&old_pwd] { MUST(Core::System::chdir(old_pwd)); };
|
||||
|
||||
auto tar_stream = TRY(Archive::TarInputStream::construct(move(file)));
|
||||
|
||||
HashMap<ByteString, ByteString> global_overrides;
|
||||
HashMap<ByteString, ByteString> local_overrides;
|
||||
|
||||
auto get_override = [&](StringView key) -> Optional<ByteString> {
|
||||
Optional<ByteString> maybe_local = local_overrides.get(key);
|
||||
|
||||
if (maybe_local.has_value())
|
||||
return maybe_local;
|
||||
|
||||
Optional<ByteString> maybe_global = global_overrides.get(key);
|
||||
|
||||
if (maybe_global.has_value())
|
||||
return maybe_global;
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
while (!tar_stream->finished()) {
|
||||
Archive::TarFileHeader const& header = tar_stream->header();
|
||||
|
||||
// Handle meta-entries earlier to avoid consuming the file content stream.
|
||||
if (header.content_is_like_extended_header()) {
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::GlobalExtendedHeader: {
|
||||
TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) {
|
||||
if (value.length() == 0)
|
||||
global_overrides.remove(key);
|
||||
else
|
||||
global_overrides.set(key, value);
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::ExtendedHeader: {
|
||||
TRY(tar_stream->for_each_extended_header([&](StringView key, StringView value) {
|
||||
local_overrides.set(key, value);
|
||||
}));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
warnln("Unknown extended header type '{}' of {}", (char)header.type_flag(), header.filename());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
TRY(tar_stream->advance());
|
||||
continue;
|
||||
}
|
||||
|
||||
Archive::TarFileStream file_stream = tar_stream->file_contents();
|
||||
|
||||
// Handle other header types that don't just have an effect on extraction.
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::LongName: {
|
||||
StringBuilder long_name;
|
||||
|
||||
Array<u8, buffer_size> buffer;
|
||||
|
||||
while (!file_stream.is_eof()) {
|
||||
auto slice = TRY(file_stream.read_some(buffer));
|
||||
long_name.append(reinterpret_cast<char*>(slice.data()), slice.size());
|
||||
}
|
||||
|
||||
local_overrides.set("path", long_name.to_byte_string());
|
||||
TRY(tar_stream->advance());
|
||||
continue;
|
||||
}
|
||||
default:
|
||||
// None of the relevant headers, so continue as normal.
|
||||
break;
|
||||
}
|
||||
|
||||
LexicalPath path = LexicalPath(header.filename());
|
||||
if (!header.prefix().is_empty())
|
||||
path = path.prepend(header.prefix());
|
||||
ByteString filename = get_override("path"sv).value_or(path.string());
|
||||
|
||||
ByteString absolute_path = TRY(FileSystem::absolute_path(filename));
|
||||
auto parent_path = LexicalPath(absolute_path).parent();
|
||||
auto header_mode = TRY(header.mode());
|
||||
|
||||
switch (header.type_flag()) {
|
||||
case Archive::TarFileType::NormalFile:
|
||||
case Archive::TarFileType::AlternateNormalFile: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
int fd = TRY(Core::System::open(absolute_path, O_CREAT | O_WRONLY, header_mode));
|
||||
|
||||
Array<u8, buffer_size> buffer;
|
||||
while (!file_stream.is_eof()) {
|
||||
auto slice = TRY(file_stream.read_some(buffer));
|
||||
TRY(Core::System::write(fd, slice));
|
||||
}
|
||||
|
||||
TRY(Core::System::close(fd));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::SymLink: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
TRY(Core::System::symlink(header.link_name(), absolute_path));
|
||||
break;
|
||||
}
|
||||
case Archive::TarFileType::Directory: {
|
||||
MUST(Core::Directory::create(parent_path, Core::Directory::CreateDirectories::Yes));
|
||||
|
||||
auto result_or_error = Core::System::mkdir(absolute_path, header_mode);
|
||||
if (result_or_error.is_error() && result_or_error.error().code() != EEXIST)
|
||||
return result_or_error.release_error();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// FIXME: Implement other file types
|
||||
warnln("file type '{}' of {} is not yet supported", (char)header.type_flag(), header.filename());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
// Non-global headers should be cleared after every file.
|
||||
local_overrides.clear();
|
||||
|
||||
TRY(tar_stream->advance());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
12
UI/Android/src/main/cpp/LadybirdServiceBase.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <jni.h>
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket);
|
||||
53
UI/Android/src/main/cpp/LadybirdServiceBaseJNI.cpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <AK/Atomic.h>
|
||||
#include <AK/Format.h>
|
||||
#include <LibCore/ResourceImplementationFile.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <jni.h>
|
||||
|
||||
JavaVM* global_vm;
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_nativeThreadLoop(JNIEnv*, jobject /* thiz */, jint);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_nativeThreadLoop(JNIEnv*, jobject /* thiz */, jint ipc_socket)
|
||||
{
|
||||
auto ret = service_main(ipc_socket);
|
||||
if (ret.is_error()) {
|
||||
warnln("Runtime Error: {}", ret.release_error());
|
||||
} else {
|
||||
outln("Thread exited with code {}", ret.release_value());
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_initNativeCode(JNIEnv*, jobject /* thiz */, jstring, jstring);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_LadybirdServiceBase_initNativeCode(JNIEnv* env, jobject /* thiz */, jstring resource_dir, jstring tag_name)
|
||||
{
|
||||
static Atomic<bool> s_initialized_flag { false };
|
||||
if (s_initialized_flag.exchange(true) == true) {
|
||||
// Skip initializing if someone else already started the process at some point in the past
|
||||
return;
|
||||
}
|
||||
|
||||
env->GetJavaVM(&global_vm);
|
||||
|
||||
char const* raw_resource_dir = env->GetStringUTFChars(resource_dir, nullptr);
|
||||
s_ladybird_resource_root = raw_resource_dir;
|
||||
env->ReleaseStringUTFChars(resource_dir, raw_resource_dir);
|
||||
// FIXME: Use a custom Android version that uses AssetManager to load files.
|
||||
Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::formatted("{}/res", s_ladybird_resource_root))));
|
||||
|
||||
char const* raw_tag_name = env->GetStringUTFChars(tag_name, nullptr);
|
||||
AK::set_log_tag_name(raw_tag_name);
|
||||
env->ReleaseStringUTFChars(tag_name, raw_tag_name);
|
||||
}
|
||||
47
UI/Android/src/main/cpp/RequestServerService.cpp
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibFileSystem/FileSystem.h>
|
||||
#include <LibIPC/SingleServer.h>
|
||||
#include <LibTLS/Certificate.h>
|
||||
#include <RequestServer/ConnectionFromClient.h>
|
||||
#include <RequestServer/HttpProtocol.h>
|
||||
#include <RequestServer/HttpsProtocol.h>
|
||||
#include <UI/Utilities.h>
|
||||
|
||||
// FIXME: Share b/w RequestServer and WebSocket
|
||||
static ErrorOr<ByteString> find_certificates(StringView serenity_resource_root)
|
||||
{
|
||||
auto cert_path = ByteString::formatted("{}/res/ladybird/cacert.pem", serenity_resource_root);
|
||||
if (!FileSystem::exists(cert_path))
|
||||
return Error::from_string_literal("Don't know how to load certs!");
|
||||
return cert_path;
|
||||
}
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket)
|
||||
{
|
||||
// Ensure the certificates are read out here.
|
||||
DefaultRootCACertificates::set_default_certificate_paths(Vector { TRY(find_certificates(s_ladybird_resource_root)) });
|
||||
[[maybe_unused]] auto& certs = DefaultRootCACertificates::the();
|
||||
|
||||
Core::EventLoop event_loop;
|
||||
|
||||
RequestServer::HttpProtocol::install();
|
||||
RequestServer::HttpsProtocol::install();
|
||||
|
||||
auto socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
|
||||
auto client = TRY(RequestServer::ConnectionFromClient::try_create(move(socket)));
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
||||
38
UI/Android/src/main/cpp/TimerExecutorService.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "ALooperEventLoopImplementation.h"
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_TimerExecutorService_00024Timer_nativeRun(JNIEnv*, jobject /* thiz */, jlong, jlong);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_TimerExecutorService_00024Timer_nativeRun(JNIEnv*, jobject /* thiz */, jlong native_data, jlong id)
|
||||
{
|
||||
static Core::EventLoop s_event_loop; // Here to exist for this thread
|
||||
|
||||
auto& event_loop_impl = *reinterpret_cast<Ladybird::ALooperEventLoopImplementation*>(native_data);
|
||||
auto& thread_data = event_loop_impl.thread_data();
|
||||
|
||||
if (auto timer_data = thread_data.timers.get(id); timer_data.has_value()) {
|
||||
auto receiver = timer_data->receiver.strong_ref();
|
||||
if (!receiver)
|
||||
return;
|
||||
|
||||
if (timer_data->visibility == Core::TimerShouldFireWhenNotVisible::No)
|
||||
if (!receiver->is_visible_for_timer_purposes())
|
||||
return;
|
||||
|
||||
event_loop_impl.post_event(*receiver, make<Core::TimerEvent>());
|
||||
}
|
||||
// Flush the event loop on this thread to keep any garbage from building up
|
||||
if (auto num_events = s_event_loop.pump(Core::EventLoop::WaitMode::PollForEvents); num_events != 0) {
|
||||
dbgln("BUG: Processed {} events on Timer thread!", num_events);
|
||||
}
|
||||
}
|
||||
158
UI/Android/src/main/cpp/WebContentService.cpp
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebContentService.h"
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include <AK/LexicalPath.h>
|
||||
#include <LibCore/ArgsParser.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/LocalServer.h>
|
||||
#include <LibCore/System.h>
|
||||
#include <LibIPC/ConnectionFromClient.h>
|
||||
#include <LibImageDecoderClient/Client.h>
|
||||
#include <LibJS/Bytecode/Interpreter.h>
|
||||
#include <LibMedia/Audio/Loader.h>
|
||||
#include <LibRequests/RequestClient.h>
|
||||
#include <LibWeb/Bindings/MainThreadVM.h>
|
||||
#include <LibWeb/HTML/Window.h>
|
||||
#include <LibWeb/Loader/ContentFilter.h>
|
||||
#include <LibWeb/Loader/GeneratedPagesLoader.h>
|
||||
#include <LibWeb/Loader/ResourceLoader.h>
|
||||
#include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
|
||||
#include <LibWeb/Platform/AudioCodecPluginAgnostic.h>
|
||||
#include <LibWeb/Platform/EventLoopPluginSerenity.h>
|
||||
#include <LibWebView/RequestServerAdapter.h>
|
||||
#include <UI/FontPlugin.h>
|
||||
#include <UI/HelperProcess.h>
|
||||
#include <UI/ImageCodecPlugin.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <WebContent/ConnectionFromClient.h>
|
||||
#include <WebContent/PageHost.h>
|
||||
|
||||
static ErrorOr<NonnullRefPtr<Requests::RequestClient>> bind_request_server_service()
|
||||
{
|
||||
return bind_service<Requests::RequestClient>(&bind_request_server_java);
|
||||
}
|
||||
|
||||
static ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> bind_image_decoder_service()
|
||||
{
|
||||
return bind_service<ImageDecoderClient::Client>(&bind_image_decoder_java);
|
||||
}
|
||||
|
||||
static ErrorOr<void> load_content_filters();
|
||||
|
||||
static ErrorOr<void> load_autoplay_allowlist();
|
||||
|
||||
ErrorOr<int> service_main(int ipc_socket)
|
||||
{
|
||||
Core::EventLoop event_loop;
|
||||
|
||||
Web::Platform::EventLoopPlugin::install(*new Web::Platform::EventLoopPluginSerenity);
|
||||
|
||||
auto image_decoder_client = TRY(bind_image_decoder_service());
|
||||
Web::Platform::ImageCodecPlugin::install(*new Ladybird::ImageCodecPlugin(move(image_decoder_client)));
|
||||
|
||||
Web::Platform::AudioCodecPlugin::install_creation_hook([](auto loader) {
|
||||
return Web::Platform::AudioCodecPluginAgnostic::create(move(loader));
|
||||
});
|
||||
|
||||
auto request_server_client = TRY(bind_request_server_service());
|
||||
Web::ResourceLoader::initialize(TRY(WebView::RequestServerAdapter::try_create(move(request_server_client))));
|
||||
|
||||
bool is_layout_test_mode = false;
|
||||
|
||||
Web::HTML::Window::set_internals_object_exposed(is_layout_test_mode);
|
||||
Web::Platform::FontPlugin::install(*new Ladybird::FontPlugin(is_layout_test_mode));
|
||||
|
||||
TRY(Web::Bindings::initialize_main_thread_vm(Web::HTML::EventLoop::Type::Window));
|
||||
|
||||
auto maybe_content_filter_error = load_content_filters();
|
||||
if (maybe_content_filter_error.is_error())
|
||||
dbgln("Failed to load content filters: {}", maybe_content_filter_error.error());
|
||||
|
||||
auto maybe_autoplay_allowlist_error = load_autoplay_allowlist();
|
||||
if (maybe_autoplay_allowlist_error.is_error())
|
||||
dbgln("Failed to load autoplay allowlist: {}", maybe_autoplay_allowlist_error.error());
|
||||
|
||||
auto webcontent_socket = TRY(Core::LocalSocket::adopt_fd(ipc_socket));
|
||||
auto webcontent_client = TRY(WebContent::ConnectionFromClient::try_create(move(webcontent_socket)));
|
||||
|
||||
return event_loop.exec();
|
||||
}
|
||||
|
||||
template<typename Client>
|
||||
ErrorOr<NonnullRefPtr<Client>> bind_service(void (*bind_method)(int))
|
||||
{
|
||||
int socket_fds[2] {};
|
||||
TRY(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds));
|
||||
|
||||
int ui_fd = socket_fds[0];
|
||||
int server_fd = socket_fds[1];
|
||||
|
||||
// NOTE: The java object takes ownership of the socket fds
|
||||
(*bind_method)(server_fd);
|
||||
|
||||
auto socket = TRY(Core::LocalSocket::adopt_fd(ui_fd));
|
||||
TRY(socket->set_blocking(true));
|
||||
|
||||
auto new_client = TRY(try_make_ref_counted<Client>(move(socket)));
|
||||
|
||||
return new_client;
|
||||
}
|
||||
|
||||
static ErrorOr<void> load_content_filters()
|
||||
{
|
||||
auto file_or_error = Core::File::open(ByteString::formatted("{}/res/ladybird/default-config/BrowserContentFilters.txt", s_ladybird_resource_root), Core::File::OpenMode::Read);
|
||||
if (file_or_error.is_error())
|
||||
return file_or_error.release_error();
|
||||
|
||||
auto file = file_or_error.release_value();
|
||||
auto ad_filter_list = TRY(Core::InputBufferedFile::create(move(file)));
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(4096));
|
||||
|
||||
Vector<String> patterns;
|
||||
|
||||
while (TRY(ad_filter_list->can_read_line())) {
|
||||
auto line = TRY(ad_filter_list->read_line(buffer));
|
||||
if (line.is_empty())
|
||||
continue;
|
||||
|
||||
auto pattern = TRY(String::from_utf8(line));
|
||||
TRY(patterns.try_append(move(pattern)));
|
||||
}
|
||||
|
||||
auto& content_filter = Web::ContentFilter::the();
|
||||
TRY(content_filter.set_patterns(patterns));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static ErrorOr<void> load_autoplay_allowlist()
|
||||
{
|
||||
auto file_or_error = Core::File::open(TRY(String::formatted("{}/res/ladybird/default-config/BrowserAutoplayAllowlist.txt", s_ladybird_resource_root)), Core::File::OpenMode::Read);
|
||||
if (file_or_error.is_error())
|
||||
return file_or_error.release_error();
|
||||
|
||||
auto file = file_or_error.release_value();
|
||||
auto allowlist = TRY(Core::InputBufferedFile::create(move(file)));
|
||||
auto buffer = TRY(ByteBuffer::create_uninitialized(4096));
|
||||
|
||||
Vector<String> origins;
|
||||
|
||||
while (TRY(allowlist->can_read_line())) {
|
||||
auto line = TRY(allowlist->read_line(buffer));
|
||||
if (line.is_empty())
|
||||
continue;
|
||||
|
||||
auto domain = TRY(String::from_utf8(line));
|
||||
TRY(origins.try_append(move(domain)));
|
||||
}
|
||||
|
||||
auto& autoplay_allowlist = Web::PermissionsPolicy::AutoplayAllowlist::the();
|
||||
TRY(autoplay_allowlist.enable_for_origins(origins));
|
||||
|
||||
return {};
|
||||
}
|
||||
15
UI/Android/src/main/cpp/WebContentService.h
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/NonnullRefPtr.h>
|
||||
|
||||
template<typename Client>
|
||||
ErrorOr<NonnullRefPtr<Client>> bind_service(void (*bind_method)(int));
|
||||
|
||||
void bind_request_server_java(int ipc_socket);
|
||||
void bind_image_decoder_java(int ipc_socket);
|
||||
52
UI/Android/src/main/cpp/WebContentServiceJNI.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "JNIHelpers.h"
|
||||
#include "LadybirdServiceBase.h"
|
||||
#include "WebContentService.h"
|
||||
#include <jni.h>
|
||||
|
||||
jobject global_instance;
|
||||
jclass global_class_reference;
|
||||
jmethodID bind_request_server_method;
|
||||
jmethodID bind_image_decoder_method;
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebContentService_nativeInit(JNIEnv*, jobject);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebContentService_nativeInit(JNIEnv* env, jobject thiz)
|
||||
{
|
||||
global_instance = env->NewGlobalRef(thiz);
|
||||
|
||||
auto local_class = env->FindClass("org/serenityos/ladybird/WebContentService");
|
||||
if (!local_class)
|
||||
TODO();
|
||||
global_class_reference = reinterpret_cast<jclass>(env->NewGlobalRef(local_class));
|
||||
env->DeleteLocalRef(local_class);
|
||||
|
||||
auto method = env->GetMethodID(global_class_reference, "bindRequestServer", "(I)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
bind_request_server_method = method;
|
||||
|
||||
method = env->GetMethodID(global_class_reference, "bindImageDecoder", "(I)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
bind_image_decoder_method = method;
|
||||
}
|
||||
|
||||
void bind_request_server_java(int ipc_socket)
|
||||
{
|
||||
Ladybird::JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(global_instance, bind_request_server_method, ipc_socket);
|
||||
}
|
||||
|
||||
void bind_image_decoder_java(int ipc_socket)
|
||||
{
|
||||
Ladybird::JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(global_instance, bind_image_decoder_method, ipc_socket);
|
||||
}
|
||||
145
UI/Android/src/main/cpp/WebViewImplementationNative.cpp
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebViewImplementationNative.h"
|
||||
#include "JNIHelpers.h"
|
||||
#include <LibGfx/Bitmap.h>
|
||||
#include <LibGfx/DeprecatedPainter.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <LibWebView/WebContentClient.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
static Gfx::BitmapFormat to_gfx_bitmap_format(i32 f)
|
||||
{
|
||||
switch (f) {
|
||||
case ANDROID_BITMAP_FORMAT_RGBA_8888:
|
||||
return Gfx::BitmapFormat::BGRA8888;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
WebViewImplementationNative::WebViewImplementationNative(jobject thiz)
|
||||
: m_java_instance(thiz)
|
||||
{
|
||||
// NOTE: m_java_instance's global ref is controlled by the JNI bindings
|
||||
initialize_client(CreateNewClient::Yes);
|
||||
|
||||
on_ready_to_paint = [this]() {
|
||||
JavaEnvironment env(global_vm);
|
||||
env.get()->CallVoidMethod(m_java_instance, invalidate_layout_method);
|
||||
};
|
||||
|
||||
on_load_start = [this](URL::URL const& url, bool is_redirect) {
|
||||
JavaEnvironment env(global_vm);
|
||||
auto url_string = env.jstring_from_ak_string(MUST(url.to_string()));
|
||||
env.get()->CallVoidMethod(m_java_instance, on_load_start_method, url_string, is_redirect);
|
||||
env.get()->DeleteLocalRef(url_string);
|
||||
};
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::initialize_client(WebView::ViewImplementation::CreateNewClient)
|
||||
{
|
||||
m_client_state = {};
|
||||
|
||||
auto new_client = bind_web_content_client();
|
||||
|
||||
m_client_state.client = new_client;
|
||||
m_client_state.client->on_web_content_process_crash = [] {
|
||||
warnln("WebContent crashed!");
|
||||
// FIXME: launch a new client
|
||||
};
|
||||
|
||||
m_client_state.client_handle = MUST(Web::Crypto::generate_random_uuid());
|
||||
client().async_set_window_handle(0, m_client_state.client_handle);
|
||||
|
||||
client().async_set_device_pixels_per_css_pixel(0, m_device_pixel_ratio);
|
||||
|
||||
// FIXME: update_palette, update system fonts
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::paint_into_bitmap(void* android_bitmap_raw, AndroidBitmapInfo const& info)
|
||||
{
|
||||
// Software bitmaps only for now!
|
||||
VERIFY((info.flags & ANDROID_BITMAP_FLAGS_IS_HARDWARE) == 0);
|
||||
|
||||
auto android_bitmap = MUST(Gfx::Bitmap::create_wrapper(to_gfx_bitmap_format(info.format), Gfx::AlphaType::Premultiplied, { info.width, info.height }, info.stride, android_bitmap_raw));
|
||||
Gfx::DeprecatedPainter painter(android_bitmap);
|
||||
if (auto* bitmap = m_client_state.has_usable_bitmap ? m_client_state.front_bitmap.bitmap.ptr() : m_backup_bitmap.ptr())
|
||||
painter.blit({ 0, 0 }, *bitmap, bitmap->rect());
|
||||
else
|
||||
painter.clear_rect(painter.clip_rect(), Gfx::Color::Magenta);
|
||||
|
||||
// Convert our internal BGRA into RGBA. This will be slowwwwwww
|
||||
// FIXME: Don't do a color format swap here.
|
||||
for (auto y = 0; y < android_bitmap->height(); ++y) {
|
||||
auto* scanline = android_bitmap->scanline(y);
|
||||
for (auto x = 0; x < android_bitmap->width(); ++x) {
|
||||
auto current_pixel = scanline[x];
|
||||
u32 alpha = (current_pixel & 0xFF000000U) >> 24;
|
||||
u32 red = (current_pixel & 0x00FF0000U) >> 16;
|
||||
u32 green = (current_pixel & 0x0000FF00U) >> 8;
|
||||
u32 blue = (current_pixel & 0x000000FFU);
|
||||
scanline[x] = (alpha << 24U) | (blue << 16U) | (green << 8U) | red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::set_viewport_geometry(int w, int h)
|
||||
{
|
||||
m_viewport_size = { w, h };
|
||||
handle_resize();
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::set_device_pixel_ratio(float f)
|
||||
{
|
||||
m_device_pixel_ratio = f;
|
||||
client().async_set_device_pixels_per_css_pixel(0, m_device_pixel_ratio);
|
||||
}
|
||||
|
||||
void WebViewImplementationNative::mouse_event(Web::MouseEvent::Type event_type, float x, float y, float raw_x, float raw_y)
|
||||
{
|
||||
Gfx::IntPoint position = { x, y };
|
||||
Gfx::IntPoint screen_position = { raw_x, raw_y };
|
||||
auto event = Web::MouseEvent {
|
||||
event_type,
|
||||
position.to_type<Web::DevicePixels>(),
|
||||
screen_position.to_type<Web::DevicePixels>(),
|
||||
Web::UIEvents::MouseButton::Primary,
|
||||
Web::UIEvents::MouseButton::Primary,
|
||||
Web::UIEvents::KeyModifier::Mod_None,
|
||||
0,
|
||||
0,
|
||||
nullptr
|
||||
};
|
||||
|
||||
enqueue_input_event(move(event));
|
||||
}
|
||||
|
||||
NonnullRefPtr<WebView::WebContentClient> WebViewImplementationNative::bind_web_content_client()
|
||||
{
|
||||
JavaEnvironment env(global_vm);
|
||||
|
||||
int socket_fds[2] {};
|
||||
MUST(Core::System::socketpair(AF_LOCAL, SOCK_STREAM, 0, socket_fds));
|
||||
|
||||
int ui_fd = socket_fds[0];
|
||||
int wc_fd = socket_fds[1];
|
||||
|
||||
// NOTE: The java object takes ownership of the socket fds
|
||||
env.get()->CallVoidMethod(m_java_instance, bind_webcontent_method, wc_fd);
|
||||
|
||||
auto socket = MUST(Core::LocalSocket::adopt_fd(ui_fd));
|
||||
MUST(socket->set_blocking(true));
|
||||
|
||||
auto new_client = make_ref_counted<WebView::WebContentClient>(move(socket), *this);
|
||||
|
||||
return new_client;
|
||||
}
|
||||
}
|
||||
45
UI/Android/src/main/cpp/WebViewImplementationNative.h
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <android/bitmap.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace Ladybird {
|
||||
class WebViewImplementationNative : public WebView::ViewImplementation {
|
||||
public:
|
||||
WebViewImplementationNative(jobject thiz);
|
||||
|
||||
virtual Web::DevicePixelSize viewport_size() const override { return m_viewport_size; }
|
||||
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint p) const override { return p; }
|
||||
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint p) const override { return p; }
|
||||
virtual void update_zoom() override { }
|
||||
|
||||
NonnullRefPtr<WebView::WebContentClient> bind_web_content_client();
|
||||
|
||||
virtual void initialize_client(CreateNewClient) override;
|
||||
|
||||
void paint_into_bitmap(void* android_bitmap_raw, AndroidBitmapInfo const& info);
|
||||
|
||||
void set_viewport_geometry(int w, int h);
|
||||
void set_device_pixel_ratio(float f);
|
||||
|
||||
void mouse_event(Web::MouseEvent::Type event_type, float x, float y, float raw_x, float raw_y);
|
||||
|
||||
static jclass global_class_reference;
|
||||
static jmethodID bind_webcontent_method;
|
||||
static jmethodID invalidate_layout_method;
|
||||
static jmethodID on_load_start_method;
|
||||
|
||||
jobject java_instance() const { return m_java_instance; }
|
||||
|
||||
private:
|
||||
jobject m_java_instance = nullptr;
|
||||
Web::DevicePixelSize m_viewport_size;
|
||||
};
|
||||
}
|
||||
145
UI/Android/src/main/cpp/WebViewImplementationNativeJNI.cpp
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include "WebViewImplementationNative.h"
|
||||
#include <jni.h>
|
||||
|
||||
using namespace Ladybird;
|
||||
|
||||
jclass WebViewImplementationNative::global_class_reference;
|
||||
jmethodID WebViewImplementationNative::bind_webcontent_method;
|
||||
jmethodID WebViewImplementationNative::invalidate_layout_method;
|
||||
jmethodID WebViewImplementationNative::on_load_start_method;
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_00024Companion_nativeClassInit(JNIEnv*, jobject /* thiz */);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_00024Companion_nativeClassInit(JNIEnv* env, jobject /* thiz */)
|
||||
{
|
||||
auto local_class = env->FindClass("org/serenityos/ladybird/WebViewImplementation");
|
||||
if (!local_class)
|
||||
TODO();
|
||||
WebViewImplementationNative::global_class_reference = reinterpret_cast<jclass>(env->NewGlobalRef(local_class));
|
||||
env->DeleteLocalRef(local_class);
|
||||
|
||||
auto method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "bindWebContentService", "(I)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
WebViewImplementationNative::bind_webcontent_method = method;
|
||||
|
||||
method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "invalidateLayout", "()V");
|
||||
if (!method)
|
||||
TODO();
|
||||
WebViewImplementationNative::invalidate_layout_method = method;
|
||||
|
||||
method = env->GetMethodID(WebViewImplementationNative::global_class_reference, "onLoadStart", "(Ljava/lang/String;Z)V");
|
||||
if (!method)
|
||||
TODO();
|
||||
WebViewImplementationNative::on_load_start_method = method;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv*, jobject);
|
||||
|
||||
extern "C" JNIEXPORT jlong JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectInit(JNIEnv* env, jobject thiz)
|
||||
{
|
||||
auto ref = env->NewGlobalRef(thiz);
|
||||
auto instance = reinterpret_cast<jlong>(new WebViewImplementationNative(ref));
|
||||
return instance;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv*, jobject /* thiz */, jlong);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeObjectDispose(JNIEnv* env, jobject /* thiz */, jlong instance)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
env->DeleteGlobalRef(impl->java_instance());
|
||||
delete impl;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeDrawIntoBitmap(JNIEnv*, jobject /* thiz */, jlong, jobject);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeDrawIntoBitmap(JNIEnv* env, jobject /* thiz */, jlong instance, jobject bitmap)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
|
||||
AndroidBitmapInfo bitmap_info = {};
|
||||
void* pixels = nullptr;
|
||||
AndroidBitmap_getInfo(env, bitmap, &bitmap_info);
|
||||
AndroidBitmap_lockPixels(env, bitmap, &pixels);
|
||||
if (pixels)
|
||||
impl->paint_into_bitmap(pixels, bitmap_info);
|
||||
|
||||
AndroidBitmap_unlockPixels(env, bitmap);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeSetViewportGeometry(JNIEnv*, jobject /* thiz */, jlong, jint, jint);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeSetViewportGeometry(JNIEnv*, jobject /* thiz */, jlong instance, jint w, jint h)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
impl->set_viewport_geometry(w, h);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeLoadURL(JNIEnv*, jobject /* thiz */, jlong, jstring);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeLoadURL(JNIEnv* env, jobject /* thiz */, jlong instance, jstring url)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
char const* raw_url = env->GetStringUTFChars(url, nullptr);
|
||||
auto ak_url = URL::create_with_url_or_path(StringView { raw_url, strlen(raw_url) });
|
||||
env->ReleaseStringUTFChars(url, raw_url);
|
||||
impl->load(ak_url);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeSetDevicePixelRatio(JNIEnv*, jobject /* thiz */, jlong instance, jfloat);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeSetDevicePixelRatio(JNIEnv*, jobject /* thiz */, jlong instance, jfloat ratio)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
impl->set_device_pixel_ratio(ratio);
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeMouseEvent(JNIEnv*, jobject /* thiz */, jlong, jint, jfloat, jfloat, jfloat, jfloat);
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_org_serenityos_ladybird_WebViewImplementation_nativeMouseEvent(JNIEnv*, jobject /* thiz */, jlong instance, jint event_type, jfloat x, jfloat y, jfloat raw_x, jfloat raw_y)
|
||||
{
|
||||
auto* impl = reinterpret_cast<WebViewImplementationNative*>(instance);
|
||||
|
||||
Web::MouseEvent::Type web_event_type;
|
||||
|
||||
// These integers are defined in Android's MotionEvent.
|
||||
// See https://developer.android.com/reference/android/view/MotionEvent#constants_1
|
||||
if (event_type == 0) {
|
||||
// MotionEvent.ACTION_DOWN
|
||||
web_event_type = Web::MouseEvent::Type::MouseDown;
|
||||
} else if (event_type == 1) {
|
||||
// MotionEvent.ACTION_UP
|
||||
web_event_type = Web::MouseEvent::Type::MouseUp;
|
||||
} else if (event_type == 2) {
|
||||
// MotionEvent.ACTION_MOVE
|
||||
web_event_type = Web::MouseEvent::Type::MouseMove;
|
||||
} else {
|
||||
// Unknown event type, default to MouseUp
|
||||
web_event_type = Web::MouseEvent::Type::MouseUp;
|
||||
}
|
||||
|
||||
impl->mouse_event(web_event_type, x, y, raw_x, raw_y);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.os.Message
|
||||
|
||||
class ImageDecoderService : LadybirdServiceBase("ImageDecoderService") {
|
||||
override fun handleServiceSpecificMessage(msg: Message): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("imagedecoder")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import org.serenityos.ladybird.databinding.ActivityMainBinding
|
||||
|
||||
class LadybirdActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var resourceDir: String
|
||||
private lateinit var view: WebView
|
||||
private lateinit var urlEditText: EditText
|
||||
private var timerService = TimerExecutorService()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
resourceDir = TransferAssets.transferAssets(this)
|
||||
initNativeCode(resourceDir, "Ladybird", timerService)
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
urlEditText = binding.urlEditText
|
||||
view = binding.webView
|
||||
view.onLoadStart = { url: String, _ ->
|
||||
urlEditText.setText(url, TextView.BufferType.EDITABLE)
|
||||
}
|
||||
urlEditText.setOnEditorActionListener { textView: TextView, actionId: Int, _: KeyEvent? ->
|
||||
when (actionId) {
|
||||
EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_SEARCH -> view.loadURL(textView.text.toString())
|
||||
}
|
||||
false
|
||||
}
|
||||
view.initialize(resourceDir)
|
||||
view.loadURL(intent.dataString ?: "https://ladybird.dev")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
view.dispose()
|
||||
disposeNativeCode()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun scheduleEventLoop() {
|
||||
mainExecutor.execute {
|
||||
execMainEventLoop()
|
||||
}
|
||||
}
|
||||
|
||||
private external fun initNativeCode(
|
||||
resourceDir: String, tag: String, timerService: TimerExecutorService
|
||||
)
|
||||
|
||||
private external fun disposeNativeCode()
|
||||
private external fun execMainEventLoop()
|
||||
|
||||
companion object {
|
||||
// Used to load the 'ladybird' library on application startup.
|
||||
init {
|
||||
System.loadLibrary("Ladybird")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
const val MSG_SET_RESOURCE_ROOT = 1
|
||||
const val MSG_TRANSFER_SOCKET = 2
|
||||
|
||||
abstract class LadybirdServiceBase(protected val TAG: String) : Service() {
|
||||
private val threadPool = Executors.newCachedThreadPool()
|
||||
protected lateinit var resourceDir: String
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.i(TAG, "Creating Service")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Log.i(TAG, "Destroying Service")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
Log.i(TAG, "Start command received")
|
||||
return super.onStartCommand(intent, flags, startId)
|
||||
}
|
||||
|
||||
private fun handleTransferSockets(msg: Message) {
|
||||
val bundle = msg.data
|
||||
// FIXME: Handle garbage messages from wierd clients
|
||||
val ipcSocket = bundle.getParcelable<ParcelFileDescriptor>("IPC_SOCKET")!!
|
||||
createThread(ipcSocket)
|
||||
}
|
||||
|
||||
private fun handleSetResourceRoot(msg: Message) {
|
||||
// FIXME: Handle this being already set, not being present, etc
|
||||
resourceDir = msg.data.getString("PATH")!!
|
||||
|
||||
initNativeCode(resourceDir, TAG)
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? {
|
||||
// FIXME: Check the intent to make sure it's legit
|
||||
return Messenger(IncomingHandler(WeakReference(this))).binder
|
||||
}
|
||||
|
||||
|
||||
private fun createThread(ipcSocket: ParcelFileDescriptor) {
|
||||
threadPool.execute {
|
||||
nativeThreadLoop(ipcSocket.detachFd())
|
||||
}
|
||||
}
|
||||
|
||||
private external fun nativeThreadLoop(ipcSocket: Int)
|
||||
private external fun initNativeCode(resourceDir: String, tagName: String);
|
||||
|
||||
abstract fun handleServiceSpecificMessage(msg: Message): Boolean
|
||||
|
||||
companion object {
|
||||
|
||||
class IncomingHandler(private val service: WeakReference<LadybirdServiceBase>) :
|
||||
Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
when (msg.what) {
|
||||
MSG_TRANSFER_SOCKET -> service.get()?.handleTransferSockets(msg)
|
||||
?: super.handleMessage(msg)
|
||||
|
||||
MSG_SET_RESOURCE_ROOT -> service.get()?.handleSetResourceRoot(msg)
|
||||
?: super.handleMessage(msg)
|
||||
|
||||
else -> {
|
||||
val ret = service.get()?.handleServiceSpecificMessage(msg)
|
||||
if (ret == null || !ret)
|
||||
super.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import android.os.Message
|
||||
import android.os.Messenger
|
||||
import android.os.ParcelFileDescriptor
|
||||
|
||||
class LadybirdServiceConnection(
|
||||
private var ipcFd: Int,
|
||||
private var resourceDir: String
|
||||
) :
|
||||
ServiceConnection {
|
||||
var boundToService: Boolean = false
|
||||
var onDisconnect: () -> Unit = {}
|
||||
private var service: Messenger? = null
|
||||
|
||||
override fun onServiceConnected(className: ComponentName, svc: IBinder) {
|
||||
// This is called when the connection with the service has been
|
||||
// established, giving us the object we can use to
|
||||
// interact with the service. We are communicating with the
|
||||
// service using a Messenger, so here we get a client-side
|
||||
// representation of that from the raw IBinder object.
|
||||
service = Messenger(svc)
|
||||
boundToService = true
|
||||
|
||||
val init = Message.obtain(null, MSG_SET_RESOURCE_ROOT)
|
||||
init.data.putString("PATH", resourceDir)
|
||||
service!!.send(init)
|
||||
|
||||
val parcel = ParcelFileDescriptor.adoptFd(ipcFd)
|
||||
val msg = Message.obtain(null, MSG_TRANSFER_SOCKET)
|
||||
msg.data.putParcelable("IPC_SOCKET", parcel)
|
||||
service!!.send(msg)
|
||||
parcel.detachFd()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
// This is called when the connection with the service has been
|
||||
// unexpectedly disconnected; that is, its process crashed.
|
||||
service = null
|
||||
boundToService = false
|
||||
|
||||
// Notify owner that the service is dead
|
||||
onDisconnect()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.os.Message
|
||||
|
||||
class RequestServerService : LadybirdServiceBase("RequestServerService") {
|
||||
override fun handleServiceSpecificMessage(msg: Message): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("requestserver")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TimerExecutorService {
|
||||
|
||||
private val executor = Executors.newSingleThreadScheduledExecutor()
|
||||
|
||||
class Timer(private var nativeData: Long) : Runnable {
|
||||
override fun run() {
|
||||
nativeRun(nativeData, id)
|
||||
}
|
||||
|
||||
private external fun nativeRun(nativeData: Long, id: Long)
|
||||
var id: Long = 0
|
||||
}
|
||||
|
||||
fun registerTimer(timer: Timer, singleShot: Boolean, milliseconds: Long): Long {
|
||||
val id = ++nextId
|
||||
timer.id = id
|
||||
val handle: ScheduledFuture<*> = if (singleShot) executor.schedule(
|
||||
timer,
|
||||
milliseconds,
|
||||
TimeUnit.MILLISECONDS
|
||||
) else executor.scheduleWithFixedDelay(
|
||||
timer,
|
||||
milliseconds,
|
||||
milliseconds,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
timers[id] = handle
|
||||
return id
|
||||
}
|
||||
|
||||
fun unregisterTimer(id: Long) {
|
||||
val timer = timers[id] ?: return
|
||||
timer.cancel(false)
|
||||
}
|
||||
|
||||
private var nextId: Long = 0
|
||||
private val timers: HashMap<Long, ScheduledFuture<*>> = hashMapOf()
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/**
|
||||
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
|
||||
* <p>
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import java.lang.String;
|
||||
|
||||
public class TransferAssets {
|
||||
/**
|
||||
* @return new ladybird resource root
|
||||
*/
|
||||
static public String transferAssets(Context context) {
|
||||
Log.d("Ladybird", "Hello from java");
|
||||
Context applicationContext = context.getApplicationContext();
|
||||
File assetDir = applicationContext.getFilesDir();
|
||||
AssetManager assetManager = applicationContext.getAssets();
|
||||
if (!copyAsset(assetManager, "ladybird-assets.tar", assetDir.getAbsolutePath() + "/ladybird-assets.tar")) {
|
||||
Log.e("Ladybird", "Unable to copy assets");
|
||||
return "Invalid Assets, this won't work";
|
||||
}
|
||||
Log.d("Ladybird", "Copied ladybird-assets.tar to app-specific storage path");
|
||||
return assetDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
// ty to https://stackoverflow.com/a/22903693 for the sauce
|
||||
private static boolean copyAsset(AssetManager assetManager,
|
||||
String fromAssetPath, String toPath) {
|
||||
try {
|
||||
InputStream in = assetManager.open(fromAssetPath);
|
||||
new File(toPath).createNewFile();
|
||||
OutputStream out = new FileOutputStream(toPath);
|
||||
copyFile(in, out);
|
||||
in.close();
|
||||
out.flush();
|
||||
out.close();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyFile(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Message
|
||||
import android.util.Log
|
||||
|
||||
class WebContentService : LadybirdServiceBase("WebContentService") {
|
||||
override fun handleServiceSpecificMessage(msg: Message): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
init {
|
||||
nativeInit();
|
||||
}
|
||||
|
||||
private fun bindRequestServer(ipcFd: Int)
|
||||
{
|
||||
val connector = LadybirdServiceConnection(ipcFd, resourceDir)
|
||||
connector.onDisconnect = {
|
||||
// FIXME: Notify impl that service is dead and might need restarted
|
||||
Log.e(TAG, "RequestServer Died! :(")
|
||||
}
|
||||
// FIXME: Unbind this at some point maybe
|
||||
bindService(
|
||||
Intent(this, RequestServerService::class.java),
|
||||
connector,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
private fun bindImageDecoder(ipcFd: Int)
|
||||
{
|
||||
val connector = LadybirdServiceConnection(ipcFd, resourceDir)
|
||||
connector.onDisconnect = {
|
||||
// FIXME: Notify impl that service is dead and might need restarted
|
||||
Log.e(TAG, "ImageDecoder Died! :(")
|
||||
}
|
||||
// FIXME: Unbind this at some point maybe
|
||||
bindService(
|
||||
Intent(this, ImageDecoderService::class.java),
|
||||
connector,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
}
|
||||
|
||||
external fun nativeInit()
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("webcontent")
|
||||
}
|
||||
}
|
||||
}
|
||||
69
UI/Android/src/main/java/org/serenityos/ladybird/WebView.kt
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
|
||||
// FIXME: This should (eventually) implement NestedScrollingChild3 and ScrollingView
|
||||
class WebView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
|
||||
private val viewImpl = WebViewImplementation(this)
|
||||
private lateinit var contentBitmap: Bitmap
|
||||
var onLoadStart: (url: String, isRedirect: Boolean) -> Unit = { _, _ -> }
|
||||
|
||||
fun initialize(resourceDir: String) {
|
||||
viewImpl.initialize(resourceDir)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
viewImpl.dispose()
|
||||
}
|
||||
|
||||
fun loadURL(url: String) {
|
||||
viewImpl.loadURL(url)
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
// The native side only supports down, move, and up events.
|
||||
// So, ignore any other MotionEvents.
|
||||
if (event.action != MotionEvent.ACTION_DOWN &&
|
||||
event.action != MotionEvent.ACTION_MOVE &&
|
||||
event.action != MotionEvent.ACTION_UP) {
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
// FIXME: We are passing these through as mouse events.
|
||||
// We should really be handling them as touch events.
|
||||
// (And we should handle scrolling - right now you have tap and drag the scrollbar!)
|
||||
viewImpl.mouseEvent(event.action, event.x, event.y, event.rawX, event.rawY)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
contentBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
|
||||
val pixelDensity = context.resources.displayMetrics.density
|
||||
viewImpl.setDevicePixelRatio(pixelDensity)
|
||||
|
||||
// FIXME: Account for scroll offset when view supports scrolling
|
||||
viewImpl.setViewportGeometry(w, h)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
viewImpl.drawIntoBitmap(contentBitmap);
|
||||
canvas.drawBitmap(contentBitmap, 0f, 0f, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
package org.serenityos.ladybird
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import java.net.URL
|
||||
|
||||
/**
|
||||
* Wrapper around WebView::ViewImplementation for use by Kotlin
|
||||
*/
|
||||
class WebViewImplementation(private val view: WebView) {
|
||||
// Instance Pointer to native object, very unsafe :)
|
||||
private var nativeInstance: Long = 0
|
||||
private lateinit var resourceDir: String
|
||||
private lateinit var connection: ServiceConnection
|
||||
|
||||
fun initialize(resourceDir: String) {
|
||||
this.resourceDir = resourceDir
|
||||
nativeInstance = nativeObjectInit()
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
nativeObjectDispose(nativeInstance)
|
||||
nativeInstance = 0
|
||||
}
|
||||
|
||||
fun loadURL(url: String) {
|
||||
nativeLoadURL(nativeInstance, url)
|
||||
}
|
||||
|
||||
fun drawIntoBitmap(bitmap: Bitmap) {
|
||||
nativeDrawIntoBitmap(nativeInstance, bitmap)
|
||||
}
|
||||
|
||||
fun setViewportGeometry(w: Int, h: Int) {
|
||||
nativeSetViewportGeometry(nativeInstance, w, h)
|
||||
}
|
||||
|
||||
fun setDevicePixelRatio(ratio: Float) {
|
||||
nativeSetDevicePixelRatio(nativeInstance, ratio)
|
||||
}
|
||||
|
||||
fun mouseEvent(eventType: Int, x: Float, y: Float, rawX: Float, rawY: Float) {
|
||||
nativeMouseEvent(nativeInstance, eventType, x, y, rawX, rawY)
|
||||
}
|
||||
|
||||
// Functions called from native code
|
||||
fun bindWebContentService(ipcFd: Int) {
|
||||
val connector = LadybirdServiceConnection(ipcFd, resourceDir)
|
||||
connector.onDisconnect = {
|
||||
// FIXME: Notify impl that service is dead and might need restarted
|
||||
Log.e("WebContentView", "WebContent Died! :(")
|
||||
}
|
||||
// FIXME: Unbind this at some point maybe
|
||||
view.context.bindService(
|
||||
Intent(view.context, WebContentService::class.java),
|
||||
connector,
|
||||
Context.BIND_AUTO_CREATE
|
||||
)
|
||||
connection = connector
|
||||
}
|
||||
|
||||
fun invalidateLayout() {
|
||||
view.requestLayout()
|
||||
view.invalidate()
|
||||
}
|
||||
|
||||
fun onLoadStart(url: String, isRedirect: Boolean) {
|
||||
view.onLoadStart(url, isRedirect)
|
||||
}
|
||||
|
||||
// Functions implemented in native code
|
||||
private external fun nativeObjectInit(): Long
|
||||
private external fun nativeObjectDispose(instance: Long)
|
||||
|
||||
private external fun nativeDrawIntoBitmap(instance: Long, bitmap: Bitmap)
|
||||
private external fun nativeSetViewportGeometry(instance: Long, w: Int, h: Int)
|
||||
private external fun nativeSetDevicePixelRatio(instance: Long, ratio: Float)
|
||||
private external fun nativeLoadURL(instance: Long, url: String)
|
||||
private external fun nativeMouseEvent(instance: Long, eventType: Int, x: Float, y: Float, rawX: Float, rawY: Float)
|
||||
|
||||
companion object {
|
||||
/*
|
||||
* We use a static class initializer to allow the native code to cache some
|
||||
* field offsets. This native function looks up and caches interesting
|
||||
* class/field/method IDs. Throws on failure.
|
||||
*/
|
||||
private external fun nativeClassInit()
|
||||
|
||||
init {
|
||||
nativeClassInit()
|
||||
}
|
||||
}
|
||||
};
|
||||
170
UI/Android/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
30
UI/Android/src/main/res/drawable/ic_launcher_foreground.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
47
UI/Android/src/main/res/layout/activity_main.xml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".LadybirdActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize">
|
||||
<!-- FIXME: Add Navigation, URL bar, Tab interactions, etc -->
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_scrollFlags="scroll|snap|enterAlways">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/urlEditText"
|
||||
style="@style/Widget.AppCompat.EditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="url"
|
||||
android:ems="10"
|
||||
android:hint="@string/url_edit_default"
|
||||
android:imeOptions="actionGo|actionSearch"
|
||||
android:inputType="textUri"
|
||||
android:singleLine="true" />
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/web_view_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<org.serenityos.ladybird.WebView
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".LadybirdActivity" />
|
||||
</FrameLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
6
UI/Android/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
BIN
UI/Android/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
UI/Android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
UI/Android/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 982 B |
BIN
UI/Android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
UI/Android/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
UI/Android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
UI/Android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
19
UI/Android/src/main/res/values-night/themes.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Ladybird" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@color/grey</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
11
UI/Android/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="grey">#FF6B6B6B</color>
|
||||
</resources>
|
||||
4
UI/Android/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<resources>
|
||||
<string name="app_name">Ladybird</string>
|
||||
<string name="url_edit_default">Enter URL...</string>
|
||||
</resources>
|
||||
19
UI/Android/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Ladybird" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@color/black</item>
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<!-- Customize your theme here. -->
|
||||
<item name="android:editTextBackground">@color/white</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
13
UI/Android/src/main/res/xml/backup_rules.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample backup rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/guide/topics/data/autobackup
|
||||
for details.
|
||||
Note: This file is ignored for devices older that API 31
|
||||
See https://developer.android.com/about/versions/12/backup-restore
|
||||
-->
|
||||
<full-backup-content>
|
||||
<!--
|
||||
<include domain="sharedpref" path="."/>
|
||||
<exclude domain="sharedpref" path="device.xml"/>
|
||||
-->
|
||||
</full-backup-content>
|
||||
19
UI/Android/src/main/res/xml/data_extraction_rules.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Sample data extraction rules file; uncomment and customize as necessary.
|
||||
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
|
||||
for details.
|
||||
-->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<!-- TODO: Use <include> and <exclude> to control what is backed up.
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
-->
|
||||
</cloud-backup>
|
||||
<!--
|
||||
<device-transfer>
|
||||
<include .../>
|
||||
<exclude .../>
|
||||
</device-transfer>
|
||||
-->
|
||||
</data-extraction-rules>
|
||||
96
UI/Android/vcpkg_android.cmake
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# This file is based on the vcpkg Android example from here https://github.com/microsoft/vcpkg-docs/blob/06b496c3f24dbe651fb593a26bee50537eeaf4e5/vcpkg/examples/vcpkg_android_example_cmake_script/cmake/vcpkg_android.cmake
|
||||
# It was modified to use CMake variables instead of environment variables, because it's not possible to set environment variables in the Android Gradle plugin.
|
||||
#
|
||||
# vcpkg_android.cmake
|
||||
#
|
||||
# Helper script when using vcpkg with cmake. It should be triggered via the variable VCPKG_TARGET_ANDROID
|
||||
#
|
||||
# For example:
|
||||
# if (VCPKG_TARGET_ANDROID)
|
||||
# include("cmake/vcpkg_android.cmake")
|
||||
# endif()
|
||||
#
|
||||
# This script will:
|
||||
# 1 & 2. check the presence of needed env variables: ANDROID_NDK and VCPKG_ROOT
|
||||
# 3. set VCPKG_TARGET_TRIPLET according to ANDROID_ABI
|
||||
# 4. Combine vcpkg and Android toolchains by setting CMAKE_TOOLCHAIN_FILE
|
||||
# and VCPKG_CHAINLOAD_TOOLCHAIN_FILE
|
||||
|
||||
# Note: VCPKG_TARGET_ANDROID is not an official vcpkg variable.
|
||||
# it is introduced for the need of this script
|
||||
|
||||
if (VCPKG_TARGET_ANDROID)
|
||||
|
||||
#
|
||||
# 1. Check the presence of variable ANDROID_NDK
|
||||
#
|
||||
if (NOT DEFINED ANDROID_NDK)
|
||||
message(FATAL_ERROR "Please set CMake variable ANDROID_NDK")
|
||||
endif()
|
||||
|
||||
#
|
||||
# 2. Check the presence of environment variable VCPKG_ROOT
|
||||
#
|
||||
if (NOT DEFINED VCPKG_ROOT)
|
||||
message(FATAL_ERROR "Please set a CMake variable VCPKG_ROOT")
|
||||
endif()
|
||||
|
||||
#
|
||||
# 3. Set VCPKG_TARGET_TRIPLET according to ANDROID_ABI
|
||||
#
|
||||
# There are four different Android ABI, each of which maps to
|
||||
# a vcpkg triplet. The following table outlines the mapping from vcpkg architectures to android architectures
|
||||
#
|
||||
# |VCPKG_TARGET_TRIPLET | ANDROID_ABI |
|
||||
# |---------------------------|----------------------|
|
||||
# |arm64-android | arm64-v8a |
|
||||
# |arm-android | armeabi-v7a |
|
||||
# |x64-android | x86_64 |
|
||||
# |x86-android | x86 |
|
||||
#
|
||||
# The variable must be stored in the cache in order to successfully the two toolchains.
|
||||
#
|
||||
if (ANDROID_ABI MATCHES "arm64-v8a")
|
||||
set(VCPKG_TARGET_TRIPLET "arm64-android" CACHE STRING "" FORCE)
|
||||
elseif(ANDROID_ABI MATCHES "armeabi-v7a")
|
||||
set(VCPKG_TARGET_TRIPLET "arm-android" CACHE STRING "" FORCE)
|
||||
elseif(ANDROID_ABI MATCHES "x86_64")
|
||||
set(VCPKG_TARGET_TRIPLET "x64-android" CACHE STRING "" FORCE)
|
||||
elseif(ANDROID_ABI MATCHES "x86")
|
||||
set(VCPKG_TARGET_TRIPLET "x86-android" CACHE STRING "" FORCE)
|
||||
else()
|
||||
message(FATAL_ERROR "
|
||||
Please specify ANDROID_ABI
|
||||
For example
|
||||
cmake ... -DANDROID_ABI=armeabi-v7a
|
||||
|
||||
Possible ABIs are: arm64-v8a, armeabi-v7a, x64-android, x86-android
|
||||
")
|
||||
endif()
|
||||
message("vcpkg_android.cmake: VCPKG_TARGET_TRIPLET was set to ${VCPKG_TARGET_TRIPLET}")
|
||||
|
||||
#
|
||||
# 4. Combine vcpkg and Android toolchains
|
||||
#
|
||||
|
||||
# vcpkg and android both provide dedicated toolchains:
|
||||
#
|
||||
# vcpkg_toolchain_file=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
|
||||
# android_toolchain_file=$ANDROID_NDK/build/cmake/android.toolchain.cmake
|
||||
#
|
||||
# When using vcpkg, the vcpkg toolchain shall be specified first.
|
||||
# However, vcpkg provides a way to preload and additional toolchain,
|
||||
# with the VCPKG_CHAINLOAD_TOOLCHAIN_FILE option.
|
||||
set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE ${ANDROID_NDK}/build/cmake/android.toolchain.cmake)
|
||||
set(CMAKE_TOOLCHAIN_FILE ${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake)
|
||||
message("vcpkg_android.cmake: CMAKE_TOOLCHAIN_FILE was set to ${CMAKE_TOOLCHAIN_FILE}")
|
||||
message("vcpkg_android.cmake: VCPKG_CHAINLOAD_TOOLCHAIN_FILE was set to ${VCPKG_CHAINLOAD_TOOLCHAIN_FILE}")
|
||||
|
||||
# vcpkg depends on the environment variables ANDROID_NDK_HOME and VCPKG_ROOT being set.
|
||||
# However, we cannot set those through the Android Gradle plugin (we can only set CMake variables).
|
||||
# Therefore, we forward our CMake variables to environment variables.
|
||||
# FIXME: would be nice if vcpkg's android toolchain did not require this...
|
||||
set(ENV{ANDROID_NDK_HOME} ${ANDROID_NDK})
|
||||
set(ENV{VCPKG_ROOT} ${VCPKG_ROOT})
|
||||
|
||||
endif(VCPKG_TARGET_ANDROID)
|
||||
31
UI/AppKit/Application/Application.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Error.h>
|
||||
#include <LibIPC/Forward.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWebView/Forward.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace Ladybird {
|
||||
class WebViewBridge;
|
||||
}
|
||||
|
||||
@interface Application : NSApplication
|
||||
|
||||
- (void)setupWebViewApplication:(Main::Arguments&)arguments
|
||||
newTabPageURL:(URL::URL)new_tab_page_url;
|
||||
|
||||
- (ErrorOr<void>)launchRequestServer;
|
||||
- (ErrorOr<void>)launchImageDecoder;
|
||||
- (ErrorOr<NonnullRefPtr<WebView::WebContentClient>>)launchWebContent:(Ladybird::WebViewBridge&)web_view_bridge;
|
||||
- (ErrorOr<IPC::File>)launchWebWorker;
|
||||
|
||||
@end
|
||||
156
UI/AppKit/Application/Application.mm
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <Interface/LadybirdWebViewBridge.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
#include <LibImageDecoderClient/Client.h>
|
||||
#include <LibRequests/RequestClient.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/WebContentClient.h>
|
||||
#include <UI/HelperProcess.h>
|
||||
#include <UI/Utilities.h>
|
||||
#include <Utilities/Conversions.h>
|
||||
|
||||
#import <Application/Application.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class ApplicationBridge : public WebView::Application {
|
||||
WEB_VIEW_APPLICATION(ApplicationBridge)
|
||||
|
||||
private:
|
||||
virtual Optional<ByteString> ask_user_for_download_folder() const override
|
||||
{
|
||||
auto* panel = [NSOpenPanel openPanel];
|
||||
[panel setAllowsMultipleSelection:NO];
|
||||
[panel setCanChooseDirectories:YES];
|
||||
[panel setCanChooseFiles:NO];
|
||||
[panel setMessage:@"Select download directory"];
|
||||
|
||||
if ([panel runModal] != NSModalResponseOK)
|
||||
return {};
|
||||
|
||||
return Ladybird::ns_string_to_byte_string([[panel URL] path]);
|
||||
}
|
||||
};
|
||||
|
||||
ApplicationBridge::ApplicationBridge(Badge<WebView::Application>, Main::Arguments&)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@interface Application ()
|
||||
{
|
||||
OwnPtr<Ladybird::ApplicationBridge> m_application_bridge;
|
||||
|
||||
RefPtr<Requests::RequestClient> m_request_server_client;
|
||||
RefPtr<ImageDecoderClient::Client> m_image_decoder_client;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation Application
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)setupWebViewApplication:(Main::Arguments&)arguments
|
||||
newTabPageURL:(URL::URL)new_tab_page_url
|
||||
{
|
||||
m_application_bridge = Ladybird::ApplicationBridge::create(arguments, move(new_tab_page_url));
|
||||
}
|
||||
|
||||
- (ErrorOr<void>)launchRequestServer
|
||||
{
|
||||
auto request_server_paths = TRY(get_paths_for_helper_process("RequestServer"sv));
|
||||
m_request_server_client = TRY(launch_request_server_process(request_server_paths, s_ladybird_resource_root));
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
static ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> launch_new_image_decoder()
|
||||
{
|
||||
auto image_decoder_paths = TRY(get_paths_for_helper_process("ImageDecoder"sv));
|
||||
return launch_image_decoder_process(image_decoder_paths);
|
||||
}
|
||||
|
||||
- (ErrorOr<void>)launchImageDecoder
|
||||
{
|
||||
m_image_decoder_client = TRY(launch_new_image_decoder());
|
||||
|
||||
__weak Application* weak_self = self;
|
||||
|
||||
m_image_decoder_client->on_death = [weak_self]() {
|
||||
Application* self = weak_self;
|
||||
if (self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_image_decoder_client = nullptr;
|
||||
|
||||
if (auto err = [self launchImageDecoder]; err.is_error()) {
|
||||
dbgln("Failed to restart image decoder: {}", err.error());
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
auto num_clients = WebView::WebContentClient::client_count();
|
||||
auto new_sockets = m_image_decoder_client->send_sync_but_allow_failure<Messages::ImageDecoderServer::ConnectNewClients>(num_clients);
|
||||
if (!new_sockets || new_sockets->sockets().size() == 0) {
|
||||
dbgln("Failed to connect {} new clients to ImageDecoder", num_clients);
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
WebView::WebContentClient::for_each_client([sockets = new_sockets->take_sockets()](WebView::WebContentClient& client) mutable {
|
||||
client.async_connect_to_image_decoder(sockets.take_last());
|
||||
return IterationDecision::Continue;
|
||||
});
|
||||
};
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
- (ErrorOr<NonnullRefPtr<WebView::WebContentClient>>)launchWebContent:(Ladybird::WebViewBridge&)web_view_bridge
|
||||
{
|
||||
// FIXME: Fail to open the tab, rather than crashing the whole application if this fails
|
||||
auto request_server_socket = TRY(connect_new_request_server_client(*m_request_server_client));
|
||||
auto image_decoder_socket = TRY(connect_new_image_decoder_client(*m_image_decoder_client));
|
||||
|
||||
auto web_content_paths = TRY(get_paths_for_helper_process("WebContent"sv));
|
||||
auto web_content = TRY(launch_web_content_process(web_view_bridge, web_content_paths, move(image_decoder_socket), move(request_server_socket)));
|
||||
|
||||
return web_content;
|
||||
}
|
||||
|
||||
- (ErrorOr<IPC::File>)launchWebWorker
|
||||
{
|
||||
auto web_worker_paths = TRY(get_paths_for_helper_process("WebWorker"sv));
|
||||
auto worker_client = TRY(launch_web_worker_process(web_worker_paths, *m_request_server_client));
|
||||
|
||||
return worker_client->clone_transport();
|
||||
}
|
||||
|
||||
#pragma mark - NSApplication
|
||||
|
||||
- (void)terminate:(id)sender
|
||||
{
|
||||
Core::EventLoop::current().quit(0);
|
||||
}
|
||||
|
||||
- (void)sendEvent:(NSEvent*)event
|
||||
{
|
||||
if ([event type] == NSEventTypeApplicationDefined) {
|
||||
Core::ThreadEventQueue::current().process();
|
||||
} else {
|
||||
[super sendEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
51
UI/AppKit/Application/ApplicationDelegate.h
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Optional.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/CSS/PreferredContrast.h>
|
||||
#include <LibWeb/CSS/PreferredMotion.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWebView/Forward.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class Tab;
|
||||
@class TabController;
|
||||
|
||||
@interface ApplicationDelegate : NSObject <NSApplicationDelegate>
|
||||
|
||||
- (nullable instancetype)init;
|
||||
|
||||
- (nonnull TabController*)createNewTab:(Optional<URL::URL> const&)url
|
||||
fromTab:(nullable Tab*)tab
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab;
|
||||
|
||||
- (nonnull TabController*)createNewTab:(StringView)html
|
||||
url:(URL::URL const&)url
|
||||
fromTab:(nullable Tab*)tab
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab;
|
||||
|
||||
- (nonnull TabController*)createChildTab:(Optional<URL::URL> const&)url
|
||||
fromTab:(nonnull Tab*)tab
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
pageIndex:(u64)page_index;
|
||||
|
||||
- (void)setActiveTab:(nonnull Tab*)tab;
|
||||
- (nullable Tab*)activeTab;
|
||||
|
||||
- (void)removeTab:(nonnull TabController*)controller;
|
||||
|
||||
- (Web::CSS::PreferredColorScheme)preferredColorScheme;
|
||||
- (Web::CSS::PreferredContrast)preferredContrast;
|
||||
- (Web::CSS::PreferredMotion)preferredMotion;
|
||||
- (WebView::SearchEngine const&)searchEngine;
|
||||
|
||||
@end
|
||||
802
UI/AppKit/Application/ApplicationDelegate.mm
Normal file
|
|
@ -0,0 +1,802 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/CookieJar.h>
|
||||
#include <LibWebView/SearchEngine.h>
|
||||
|
||||
#import <Application/ApplicationDelegate.h>
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Interface/TabController.h>
|
||||
#import <LibWebView/UserAgent.h>
|
||||
|
||||
#if defined(LADYBIRD_USE_SWIFT)
|
||||
// FIXME: Report this codegen error to Apple
|
||||
# define StyleMask NSWindowStyleMask
|
||||
# import <Ladybird-Swift.h>
|
||||
# undef StyleMask
|
||||
#else
|
||||
# import <Interface/TaskManagerController.h>
|
||||
#endif
|
||||
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
@interface ApplicationDelegate () <TaskManagerDelegate>
|
||||
{
|
||||
Web::CSS::PreferredColorScheme m_preferred_color_scheme;
|
||||
Web::CSS::PreferredContrast m_preferred_contrast;
|
||||
Web::CSS::PreferredMotion m_preferred_motion;
|
||||
ByteString m_navigator_compatibility_mode;
|
||||
|
||||
WebView::SearchEngine m_search_engine;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSMutableArray<TabController*>* managed_tabs;
|
||||
@property (nonatomic, weak) Tab* active_tab;
|
||||
|
||||
@property (nonatomic, strong) TaskManagerController* task_manager_controller;
|
||||
|
||||
- (NSMenuItem*)createApplicationMenu;
|
||||
- (NSMenuItem*)createFileMenu;
|
||||
- (NSMenuItem*)createEditMenu;
|
||||
- (NSMenuItem*)createViewMenu;
|
||||
- (NSMenuItem*)createSettingsMenu;
|
||||
- (NSMenuItem*)createHistoryMenu;
|
||||
- (NSMenuItem*)createInspectMenu;
|
||||
- (NSMenuItem*)createDebugMenu;
|
||||
- (NSMenuItem*)createWindowMenu;
|
||||
- (NSMenuItem*)createHelpMenu;
|
||||
|
||||
@end
|
||||
|
||||
@implementation ApplicationDelegate
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
[NSApp setMainMenu:[[NSMenu alloc] init]];
|
||||
|
||||
[[NSApp mainMenu] addItem:[self createApplicationMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createFileMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createEditMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createViewMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createSettingsMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createHistoryMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createInspectMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createDebugMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createWindowMenu]];
|
||||
[[NSApp mainMenu] addItem:[self createHelpMenu]];
|
||||
|
||||
self.managed_tabs = [[NSMutableArray alloc] init];
|
||||
|
||||
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Auto;
|
||||
m_preferred_contrast = Web::CSS::PreferredContrast::Auto;
|
||||
m_preferred_motion = Web::CSS::PreferredMotion::Auto;
|
||||
m_navigator_compatibility_mode = "chrome";
|
||||
m_search_engine = WebView::default_search_engine();
|
||||
|
||||
// Reduce the tooltip delay, as the default delay feels quite long.
|
||||
[[NSUserDefaults standardUserDefaults] setObject:@100 forKey:@"NSInitialToolTipDelay"];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (TabController*)createNewTab:(Optional<URL::URL> const&)url
|
||||
fromTab:(Tab*)tab
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
{
|
||||
auto* controller = [self createNewTab:activate_tab fromTab:tab];
|
||||
|
||||
if (url.has_value()) {
|
||||
[controller loadURL:*url];
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (nonnull TabController*)createNewTab:(StringView)html
|
||||
url:(URL::URL const&)url
|
||||
fromTab:(nullable Tab*)tab
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
{
|
||||
auto* controller = [self createNewTab:activate_tab fromTab:tab];
|
||||
[controller loadHTML:html url:url];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (nonnull TabController*)createChildTab:(Optional<URL::URL> const&)url
|
||||
fromTab:(nonnull Tab*)tab
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
pageIndex:(u64)page_index
|
||||
{
|
||||
auto* controller = [self createChildTab:activate_tab fromTab:tab pageIndex:page_index];
|
||||
|
||||
if (url.has_value()) {
|
||||
[controller loadURL:*url];
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (void)setActiveTab:(Tab*)tab
|
||||
{
|
||||
self.active_tab = tab;
|
||||
}
|
||||
|
||||
- (Tab*)activeTab
|
||||
{
|
||||
return self.active_tab;
|
||||
}
|
||||
|
||||
- (void)removeTab:(TabController*)controller
|
||||
{
|
||||
[self.managed_tabs removeObject:controller];
|
||||
|
||||
if ([self.managed_tabs count] == 0u) {
|
||||
if (self.task_manager_controller != nil) {
|
||||
[self.task_manager_controller.window close];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (Web::CSS::PreferredColorScheme)preferredColorScheme
|
||||
{
|
||||
return m_preferred_color_scheme;
|
||||
}
|
||||
|
||||
- (Web::CSS::PreferredContrast)preferredContrast
|
||||
{
|
||||
return m_preferred_contrast;
|
||||
}
|
||||
|
||||
- (Web::CSS::PreferredMotion)preferredMotion
|
||||
{
|
||||
return m_preferred_motion;
|
||||
}
|
||||
|
||||
- (WebView::SearchEngine const&)searchEngine
|
||||
{
|
||||
return m_search_engine;
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (void)openAboutVersionPage:(id)sender
|
||||
{
|
||||
auto* current_tab = [NSApp keyWindow];
|
||||
if (![current_tab isKindOfClass:[Tab class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self createNewTab:URL::URL("about:version"sv)
|
||||
fromTab:(Tab*)current_tab
|
||||
activateTab:Web::HTML::ActivateTab::Yes];
|
||||
}
|
||||
|
||||
- (nonnull TabController*)createNewTab:(Web::HTML::ActivateTab)activate_tab
|
||||
fromTab:(nullable Tab*)tab
|
||||
{
|
||||
auto* controller = [[TabController alloc] init];
|
||||
[self initializeTabController:controller
|
||||
activateTab:activate_tab
|
||||
fromTab:tab];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (nonnull TabController*)createChildTab:(Web::HTML::ActivateTab)activate_tab
|
||||
fromTab:(nonnull Tab*)tab
|
||||
pageIndex:(u64)page_index
|
||||
{
|
||||
auto* controller = [[TabController alloc] initAsChild:tab pageIndex:page_index];
|
||||
[self initializeTabController:controller
|
||||
activateTab:activate_tab
|
||||
fromTab:tab];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (void)initializeTabController:(TabController*)controller
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
fromTab:(nullable Tab*)tab
|
||||
{
|
||||
[controller showWindow:nil];
|
||||
|
||||
if (tab) {
|
||||
[[tab tabGroup] addWindow:controller.window];
|
||||
|
||||
// FIXME: Can we create the tabbed window above without it becoming active in the first place?
|
||||
if (activate_tab == Web::HTML::ActivateTab::No) {
|
||||
[tab orderFront:nil];
|
||||
}
|
||||
}
|
||||
|
||||
if (activate_tab == Web::HTML::ActivateTab::Yes) {
|
||||
[[controller window] orderFrontRegardless];
|
||||
}
|
||||
|
||||
[self.managed_tabs addObject:controller];
|
||||
[controller onCreateNewTab];
|
||||
}
|
||||
|
||||
- (void)closeCurrentTab:(id)sender
|
||||
{
|
||||
auto* current_window = [NSApp keyWindow];
|
||||
[current_window close];
|
||||
}
|
||||
|
||||
- (void)openTaskManager:(id)sender
|
||||
{
|
||||
if (self.task_manager_controller != nil) {
|
||||
[self.task_manager_controller.window makeKeyAndOrderFront:sender];
|
||||
return;
|
||||
}
|
||||
|
||||
self.task_manager_controller = [[TaskManagerController alloc] initWithDelegate:self];
|
||||
[self.task_manager_controller showWindow:nil];
|
||||
}
|
||||
|
||||
- (void)openLocation:(id)sender
|
||||
{
|
||||
auto* current_tab = [NSApp keyWindow];
|
||||
|
||||
if (![current_tab isKindOfClass:[Tab class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* controller = (TabController*)[current_tab windowController];
|
||||
[controller focusLocationToolbarItem];
|
||||
}
|
||||
|
||||
- (void)setAutoPreferredColorScheme:(id)sender
|
||||
{
|
||||
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Auto;
|
||||
[self broadcastPreferredColorSchemeUpdate];
|
||||
}
|
||||
|
||||
- (void)setDarkPreferredColorScheme:(id)sender
|
||||
{
|
||||
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Dark;
|
||||
[self broadcastPreferredColorSchemeUpdate];
|
||||
}
|
||||
|
||||
- (void)setLightPreferredColorScheme:(id)sender
|
||||
{
|
||||
m_preferred_color_scheme = Web::CSS::PreferredColorScheme::Light;
|
||||
[self broadcastPreferredColorSchemeUpdate];
|
||||
}
|
||||
|
||||
- (void)broadcastPreferredColorSchemeUpdate
|
||||
{
|
||||
for (TabController* controller in self.managed_tabs) {
|
||||
auto* tab = (Tab*)[controller window];
|
||||
[[tab web_view] setPreferredColorScheme:m_preferred_color_scheme];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAutoPreferredContrast:(id)sender
|
||||
{
|
||||
m_preferred_contrast = Web::CSS::PreferredContrast::Auto;
|
||||
[self broadcastPreferredContrastUpdate];
|
||||
}
|
||||
|
||||
- (void)setLessPreferredContrast:(id)sender
|
||||
{
|
||||
m_preferred_contrast = Web::CSS::PreferredContrast::Less;
|
||||
[self broadcastPreferredContrastUpdate];
|
||||
}
|
||||
|
||||
- (void)setMorePreferredContrast:(id)sender
|
||||
{
|
||||
m_preferred_contrast = Web::CSS::PreferredContrast::More;
|
||||
[self broadcastPreferredContrastUpdate];
|
||||
}
|
||||
|
||||
- (void)setNoPreferencePreferredContrast:(id)sender
|
||||
{
|
||||
m_preferred_contrast = Web::CSS::PreferredContrast::NoPreference;
|
||||
[self broadcastPreferredContrastUpdate];
|
||||
}
|
||||
|
||||
- (void)broadcastPreferredContrastUpdate
|
||||
{
|
||||
for (TabController* controller in self.managed_tabs) {
|
||||
auto* tab = (Tab*)[controller window];
|
||||
[[tab web_view] setPreferredContrast:m_preferred_contrast];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAutoPreferredMotion:(id)sender
|
||||
{
|
||||
m_preferred_motion = Web::CSS::PreferredMotion::Auto;
|
||||
[self broadcastPreferredMotionUpdate];
|
||||
}
|
||||
|
||||
- (void)setNoPreferencePreferredMotion:(id)sender
|
||||
{
|
||||
m_preferred_motion = Web::CSS::PreferredMotion::NoPreference;
|
||||
[self broadcastPreferredMotionUpdate];
|
||||
}
|
||||
|
||||
- (void)setReducePreferredMotion:(id)sender
|
||||
{
|
||||
m_preferred_motion = Web::CSS::PreferredMotion::Reduce;
|
||||
[self broadcastPreferredMotionUpdate];
|
||||
}
|
||||
|
||||
- (void)broadcastPreferredMotionUpdate
|
||||
{
|
||||
for (TabController* controller in self.managed_tabs) {
|
||||
auto* tab = (Tab*)[controller window];
|
||||
[[tab web_view] setPreferredMotion:m_preferred_motion];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSearchEngine:(id)sender
|
||||
{
|
||||
auto* item = (NSMenuItem*)sender;
|
||||
auto title = Ladybird::ns_string_to_string([item title]);
|
||||
|
||||
if (auto search_engine = WebView::find_search_engine_by_name(title); search_engine.has_value())
|
||||
m_search_engine = search_engine.release_value();
|
||||
else
|
||||
m_search_engine = WebView::default_search_engine();
|
||||
}
|
||||
|
||||
- (void)clearHistory:(id)sender
|
||||
{
|
||||
for (TabController* controller in self.managed_tabs) {
|
||||
[controller clearHistory];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dumpCookies:(id)sender
|
||||
{
|
||||
WebView::Application::cookie_jar().dump_cookies();
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createApplicationMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
|
||||
auto* process_name = [[NSProcessInfo processInfo] processName];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:process_name];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"About %@", process_name]
|
||||
action:@selector(openAboutVersionPage:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"Hide %@", process_name]
|
||||
action:@selector(hide:)
|
||||
keyEquivalent:@"h"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"Quit %@", process_name]
|
||||
action:@selector(terminate:)
|
||||
keyEquivalent:@"q"]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createFileMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"File"];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"New Tab"
|
||||
action:@selector(createNewTab:)
|
||||
keyEquivalent:@"t"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Close Tab"
|
||||
action:@selector(closeCurrentTab:)
|
||||
keyEquivalent:@"w"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Location"
|
||||
action:@selector(openLocation:)
|
||||
keyEquivalent:@"l"]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createEditMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"Edit"];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Undo"
|
||||
action:@selector(undo:)
|
||||
keyEquivalent:@"z"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Redo"
|
||||
action:@selector(redo:)
|
||||
keyEquivalent:@"y"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Cut"
|
||||
action:@selector(cut:)
|
||||
keyEquivalent:@"x"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy"
|
||||
action:@selector(copy:)
|
||||
keyEquivalent:@"c"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Paste"
|
||||
action:@selector(paste:)
|
||||
keyEquivalent:@"v"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Select All"
|
||||
action:@selector(selectAll:)
|
||||
keyEquivalent:@"a"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find..."
|
||||
action:@selector(find:)
|
||||
keyEquivalent:@"f"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find Next"
|
||||
action:@selector(findNextMatch:)
|
||||
keyEquivalent:@"g"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Find Previous"
|
||||
action:@selector(findPreviousMatch:)
|
||||
keyEquivalent:@"G"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Use Selection for Find"
|
||||
action:@selector(useSelectionForFind:)
|
||||
keyEquivalent:@"e"]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createViewMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"View"];
|
||||
|
||||
auto* color_scheme_menu = [[NSMenu alloc] init];
|
||||
[color_scheme_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Auto"
|
||||
action:@selector(setAutoPreferredColorScheme:)
|
||||
keyEquivalent:@""]];
|
||||
[color_scheme_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Dark"
|
||||
action:@selector(setDarkPreferredColorScheme:)
|
||||
keyEquivalent:@""]];
|
||||
[color_scheme_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Light"
|
||||
action:@selector(setLightPreferredColorScheme:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
auto* color_scheme_menu_item = [[NSMenuItem alloc] initWithTitle:@"Color Scheme"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[color_scheme_menu_item setSubmenu:color_scheme_menu];
|
||||
|
||||
auto* contrast_menu = [[NSMenu alloc] init];
|
||||
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Auto"
|
||||
action:@selector(setAutoPreferredContrast:)
|
||||
keyEquivalent:@""]];
|
||||
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Less"
|
||||
action:@selector(setLessPreferredContrast:)
|
||||
keyEquivalent:@""]];
|
||||
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"More"
|
||||
action:@selector(setMorePreferredContrast:)
|
||||
keyEquivalent:@""]];
|
||||
[contrast_menu addItem:[[NSMenuItem alloc] initWithTitle:@"No Preference"
|
||||
action:@selector(setNoPreferencePreferredContrast:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
auto* contrast_menu_item = [[NSMenuItem alloc] initWithTitle:@"Contrast"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[contrast_menu_item setSubmenu:contrast_menu];
|
||||
|
||||
auto* motion_menu = [[NSMenu alloc] init];
|
||||
[motion_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Auto"
|
||||
action:@selector(setAutoPreferredMotion:)
|
||||
keyEquivalent:@""]];
|
||||
[motion_menu addItem:[[NSMenuItem alloc] initWithTitle:@"No Preference"
|
||||
action:@selector(setNoPreferencePreferredMotion:)
|
||||
keyEquivalent:@""]];
|
||||
[motion_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Reduce"
|
||||
action:@selector(setReducePreferredMotion:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
auto* motion_menu_item = [[NSMenuItem alloc] initWithTitle:@"Motion"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[motion_menu_item setSubmenu:motion_menu];
|
||||
|
||||
auto* zoom_menu = [[NSMenu alloc] init];
|
||||
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Zoom In"
|
||||
action:@selector(zoomIn:)
|
||||
keyEquivalent:@"+"]];
|
||||
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Zoom Out"
|
||||
action:@selector(zoomOut:)
|
||||
keyEquivalent:@"-"]];
|
||||
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Actual Size"
|
||||
action:@selector(resetZoom:)
|
||||
keyEquivalent:@"0"]];
|
||||
|
||||
auto* zoom_menu_item = [[NSMenuItem alloc] initWithTitle:@"Zoom"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[zoom_menu_item setSubmenu:zoom_menu];
|
||||
|
||||
[submenu addItem:color_scheme_menu_item];
|
||||
[submenu addItem:contrast_menu_item];
|
||||
[submenu addItem:motion_menu_item];
|
||||
[submenu addItem:zoom_menu_item];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createSettingsMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"Settings"];
|
||||
|
||||
auto* search_engine_menu = [[NSMenu alloc] init];
|
||||
|
||||
for (auto const& search_engine : WebView::search_engines()) {
|
||||
[search_engine_menu addItem:[[NSMenuItem alloc] initWithTitle:Ladybird::string_to_ns_string(search_engine.name)
|
||||
action:@selector(setSearchEngine:)
|
||||
keyEquivalent:@""]];
|
||||
}
|
||||
|
||||
auto* search_engine_menu_item = [[NSMenuItem alloc] initWithTitle:@"Search Engine"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[search_engine_menu_item setSubmenu:search_engine_menu];
|
||||
|
||||
[submenu addItem:search_engine_menu_item];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Enable Autoplay"
|
||||
action:@selector(toggleAutoplay:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createHistoryMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"History"];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Reload Page"
|
||||
action:@selector(reload:)
|
||||
keyEquivalent:@"r"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Navigate Back"
|
||||
action:@selector(navigateBack:)
|
||||
keyEquivalent:@"["]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Navigate Forward"
|
||||
action:@selector(navigateForward:)
|
||||
keyEquivalent:@"]"]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Clear History"
|
||||
action:@selector(clearHistory:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createInspectMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"Inspect"];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"View Source"
|
||||
action:@selector(viewSource:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Inspector"
|
||||
action:@selector(openInspector:)
|
||||
keyEquivalent:@"I"]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Task Manager"
|
||||
action:@selector(openTaskManager:)
|
||||
keyEquivalent:@"M"]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createDebugMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"Debug"];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump DOM Tree"
|
||||
action:@selector(dumpDOMTree:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Layout Tree"
|
||||
action:@selector(dumpLayoutTree:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Paint Tree"
|
||||
action:@selector(dumpPaintTree:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Stacking Context Tree"
|
||||
action:@selector(dumpStackingContextTree:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Style Sheets"
|
||||
action:@selector(dumpStyleSheets:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump All Resolved Styles"
|
||||
action:@selector(dumpAllResolvedStyles:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump History"
|
||||
action:@selector(dumpHistory:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Cookies"
|
||||
action:@selector(dumpCookies:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump Local Storage"
|
||||
action:@selector(dumpLocalStorage:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Show Line Box Borders"
|
||||
action:@selector(toggleLineBoxBorders:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Collect Garbage"
|
||||
action:@selector(collectGarbage:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Dump GC Graph"
|
||||
action:@selector(dumpGCGraph:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Clear Cache"
|
||||
action:@selector(clearCache:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
auto* spoof_user_agent_menu = [[NSMenu alloc] init];
|
||||
auto add_user_agent = [spoof_user_agent_menu](ByteString name) {
|
||||
[spoof_user_agent_menu addItem:[[NSMenuItem alloc] initWithTitle:Ladybird::string_to_ns_string(name)
|
||||
action:@selector(setUserAgentSpoof:)
|
||||
keyEquivalent:@""]];
|
||||
};
|
||||
|
||||
add_user_agent("Disabled");
|
||||
for (auto const& userAgent : WebView::user_agents)
|
||||
add_user_agent(userAgent.key);
|
||||
|
||||
auto* spoof_user_agent_menu_item = [[NSMenuItem alloc] initWithTitle:@"Spoof User Agent"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[spoof_user_agent_menu_item setSubmenu:spoof_user_agent_menu];
|
||||
|
||||
[submenu addItem:spoof_user_agent_menu_item];
|
||||
|
||||
auto* navigator_compatibility_mode_menu = [[NSMenu alloc] init];
|
||||
auto add_navigator_compatibility_mode = [navigator_compatibility_mode_menu](ByteString name) {
|
||||
[navigator_compatibility_mode_menu addItem:[[NSMenuItem alloc] initWithTitle:Ladybird::string_to_ns_string(name)
|
||||
action:@selector(setNavigatorCompatibilityMode:)
|
||||
keyEquivalent:@""]];
|
||||
};
|
||||
add_navigator_compatibility_mode("Chrome");
|
||||
add_navigator_compatibility_mode("Gecko");
|
||||
add_navigator_compatibility_mode("WebKit");
|
||||
|
||||
auto* navigator_compatibility_mode_menu_item = [[NSMenuItem alloc] initWithTitle:@"Navigator Compatibility Mode"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[navigator_compatibility_mode_menu_item setSubmenu:navigator_compatibility_mode_menu];
|
||||
|
||||
[submenu addItem:navigator_compatibility_mode_menu_item];
|
||||
[submenu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Enable Scripting"
|
||||
action:@selector(toggleScripting:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Block Pop-ups"
|
||||
action:@selector(togglePopupBlocking:)
|
||||
keyEquivalent:@""]];
|
||||
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Enable Same-Origin Policy"
|
||||
action:@selector(toggleSameOriginPolicy:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createWindowMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"Window"];
|
||||
|
||||
[NSApp setWindowsMenu:submenu];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
- (NSMenuItem*)createHelpMenu
|
||||
{
|
||||
auto* menu = [[NSMenuItem alloc] init];
|
||||
auto* submenu = [[NSMenu alloc] initWithTitle:@"Help"];
|
||||
|
||||
[NSApp setHelpMenu:submenu];
|
||||
|
||||
[menu setSubmenu:submenu];
|
||||
return menu;
|
||||
}
|
||||
|
||||
#pragma mark - NSApplicationDelegate
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification*)notification
|
||||
{
|
||||
Tab* tab = nil;
|
||||
|
||||
for (auto const& url : WebView::Application::chrome_options().urls) {
|
||||
auto activate_tab = tab == nil ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No;
|
||||
|
||||
auto* controller = [self createNewTab:url
|
||||
fromTab:tab
|
||||
activateTab:activate_tab];
|
||||
|
||||
tab = (Tab*)[controller window];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification*)notification
|
||||
{
|
||||
}
|
||||
|
||||
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem*)item
|
||||
{
|
||||
if ([item action] == @selector(setAutoPreferredColorScheme:)) {
|
||||
[item setState:(m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Auto) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setDarkPreferredColorScheme:)) {
|
||||
[item setState:(m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Dark) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setLightPreferredColorScheme:)) {
|
||||
[item setState:(m_preferred_color_scheme == Web::CSS::PreferredColorScheme::Light) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setAutoPreferredContrast:)) {
|
||||
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::Auto) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setLessPreferredContrast:)) {
|
||||
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::Less) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setMorePreferredContrast:)) {
|
||||
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::More) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setNoPreferencePreferredContrast:)) {
|
||||
[item setState:(m_preferred_contrast == Web::CSS::PreferredContrast::NoPreference) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setAutoPreferredMotion:)) {
|
||||
[item setState:(m_preferred_motion == Web::CSS::PreferredMotion::Auto) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setNoPreferencePreferredMotion:)) {
|
||||
[item setState:(m_preferred_motion == Web::CSS::PreferredMotion::NoPreference) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setReducePreferredMotion:)) {
|
||||
[item setState:(m_preferred_motion == Web::CSS::PreferredMotion::Reduce) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setSearchEngine:)) {
|
||||
auto title = Ladybird::ns_string_to_string([item title]);
|
||||
[item setState:(m_search_engine.name == title) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - TaskManagerDelegate
|
||||
|
||||
- (void)onTaskManagerClosed
|
||||
{
|
||||
self.task_manager_controller = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
55
UI/AppKit/Application/EventLoopImplementation.h
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/NonnullOwnPtr.h>
|
||||
#include <LibCore/EventLoopImplementation.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class CFEventLoopManager final : public Core::EventLoopManager {
|
||||
public:
|
||||
virtual NonnullOwnPtr<Core::EventLoopImplementation> make_implementation() override;
|
||||
|
||||
virtual intptr_t register_timer(Core::EventReceiver&, int interval_milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible) override;
|
||||
virtual void unregister_timer(intptr_t timer_id) override;
|
||||
|
||||
virtual void register_notifier(Core::Notifier&) override;
|
||||
virtual void unregister_notifier(Core::Notifier&) override;
|
||||
|
||||
virtual void did_post_event() override;
|
||||
|
||||
virtual int register_signal(int, Function<void(int)>) override;
|
||||
virtual void unregister_signal(int) override;
|
||||
};
|
||||
|
||||
class CFEventLoopImplementation final : public Core::EventLoopImplementation {
|
||||
public:
|
||||
// FIXME: This currently only manages the main NSApp event loop, as that is all we currently
|
||||
// interact with. When we need multiple event loops, or an event loop that isn't the
|
||||
// NSApp loop, we will need to create our own CFRunLoop.
|
||||
static NonnullOwnPtr<CFEventLoopImplementation> create();
|
||||
|
||||
virtual int exec() override;
|
||||
virtual size_t pump(PumpMode) override;
|
||||
virtual void quit(int) override;
|
||||
virtual void wake() override;
|
||||
virtual void post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&&) override;
|
||||
|
||||
// FIXME: These APIs only exist for obscure use-cases inside SerenityOS. Try to get rid of them.
|
||||
virtual void unquit() override { }
|
||||
virtual bool was_exit_requested() const override { return false; }
|
||||
virtual void notify_forked_and_in_child() override { }
|
||||
|
||||
private:
|
||||
CFEventLoopImplementation() = default;
|
||||
|
||||
int m_exit_code { 0 };
|
||||
};
|
||||
|
||||
}
|
||||
414
UI/AppKit/Application/EventLoopImplementation.mm
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/IDAllocator.h>
|
||||
#include <AK/Singleton.h>
|
||||
#include <AK/TemporaryChange.h>
|
||||
#include <LibCore/Event.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibCore/ThreadEventQueue.h>
|
||||
|
||||
#import <Application/EventLoopImplementation.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
#include <sys/event.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
struct ThreadData {
|
||||
static ThreadData& the()
|
||||
{
|
||||
static thread_local ThreadData s_thread_data;
|
||||
return s_thread_data;
|
||||
}
|
||||
|
||||
Core::Notifier& notifier_by_fd(int fd)
|
||||
{
|
||||
for (auto notifier : notifiers) {
|
||||
if (notifier.key->fd() == fd)
|
||||
return *notifier.key;
|
||||
}
|
||||
|
||||
// If we didn't have a notifier for the provided FD, it should have been unregistered.
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
IDAllocator timer_id_allocator;
|
||||
HashMap<int, CFRunLoopTimerRef> timers;
|
||||
HashMap<Core::Notifier*, CFRunLoopSourceRef> notifiers;
|
||||
};
|
||||
|
||||
class SignalHandlers : public RefCounted<SignalHandlers> {
|
||||
AK_MAKE_NONCOPYABLE(SignalHandlers);
|
||||
AK_MAKE_NONMOVABLE(SignalHandlers);
|
||||
|
||||
public:
|
||||
SignalHandlers(int signal_number, CFFileDescriptorCallBack);
|
||||
~SignalHandlers();
|
||||
|
||||
void dispatch();
|
||||
int add(Function<void(int)>&& handler);
|
||||
bool remove(int handler_id);
|
||||
|
||||
bool is_empty() const
|
||||
{
|
||||
if (m_calling_handlers) {
|
||||
for (auto const& handler : m_handlers_pending) {
|
||||
if (handler.value)
|
||||
return false; // an add is pending
|
||||
}
|
||||
}
|
||||
return m_handlers.is_empty();
|
||||
}
|
||||
|
||||
bool have(int handler_id) const
|
||||
{
|
||||
if (m_calling_handlers) {
|
||||
auto it = m_handlers_pending.find(handler_id);
|
||||
if (it != m_handlers_pending.end()) {
|
||||
if (!it->value)
|
||||
return false; // a deletion is pending
|
||||
}
|
||||
}
|
||||
return m_handlers.contains(handler_id);
|
||||
}
|
||||
|
||||
int m_signal_number;
|
||||
void (*m_original_handler)(int);
|
||||
HashMap<int, Function<void(int)>> m_handlers;
|
||||
HashMap<int, Function<void(int)>> m_handlers_pending;
|
||||
bool m_calling_handlers { false };
|
||||
CFRunLoopSourceRef m_source { nullptr };
|
||||
int m_kevent_fd = { -1 };
|
||||
};
|
||||
|
||||
SignalHandlers::SignalHandlers(int signal_number, CFFileDescriptorCallBack handle_signal)
|
||||
: m_signal_number(signal_number)
|
||||
, m_original_handler(signal(signal_number, [](int) {}))
|
||||
{
|
||||
m_kevent_fd = kqueue();
|
||||
if (m_kevent_fd < 0) {
|
||||
dbgln("Unable to create kqueue to register signal {}: {}", signal_number, strerror(errno));
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
struct kevent changes = {};
|
||||
EV_SET(&changes, signal_number, EVFILT_SIGNAL, EV_ADD | EV_RECEIPT, 0, 0, nullptr);
|
||||
if (auto res = kevent(m_kevent_fd, &changes, 1, &changes, 1, NULL); res < 0) {
|
||||
dbgln("Unable to register signal {}: {}", signal_number, strerror(errno));
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
CFFileDescriptorContext context = { 0, this, nullptr, nullptr, nullptr };
|
||||
CFFileDescriptorRef kq_ref = CFFileDescriptorCreate(kCFAllocatorDefault, m_kevent_fd, FALSE, handle_signal, &context);
|
||||
|
||||
m_source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, kq_ref, 0);
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode);
|
||||
|
||||
CFFileDescriptorEnableCallBacks(kq_ref, kCFFileDescriptorReadCallBack);
|
||||
CFRelease(kq_ref);
|
||||
}
|
||||
|
||||
SignalHandlers::~SignalHandlers()
|
||||
{
|
||||
CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode);
|
||||
CFRelease(m_source);
|
||||
(void)::signal(m_signal_number, m_original_handler);
|
||||
::close(m_kevent_fd);
|
||||
}
|
||||
|
||||
struct SignalHandlersInfo {
|
||||
HashMap<int, NonnullRefPtr<SignalHandlers>> signal_handlers;
|
||||
int next_signal_id { 0 };
|
||||
};
|
||||
|
||||
static Singleton<SignalHandlersInfo> s_signals;
|
||||
static SignalHandlersInfo* signals_info()
|
||||
{
|
||||
return s_signals.ptr();
|
||||
}
|
||||
|
||||
void SignalHandlers::dispatch()
|
||||
{
|
||||
TemporaryChange change(m_calling_handlers, true);
|
||||
for (auto& handler : m_handlers)
|
||||
handler.value(m_signal_number);
|
||||
if (!m_handlers_pending.is_empty()) {
|
||||
// Apply pending adds/removes
|
||||
for (auto& handler : m_handlers_pending) {
|
||||
if (handler.value) {
|
||||
auto result = m_handlers.set(handler.key, move(handler.value));
|
||||
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
|
||||
} else {
|
||||
m_handlers.remove(handler.key);
|
||||
}
|
||||
}
|
||||
m_handlers_pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
int SignalHandlers::add(Function<void(int)>&& handler)
|
||||
{
|
||||
int id = ++signals_info()->next_signal_id; // TODO: worry about wrapping and duplicates?
|
||||
if (m_calling_handlers)
|
||||
m_handlers_pending.set(id, move(handler));
|
||||
else
|
||||
m_handlers.set(id, move(handler));
|
||||
return id;
|
||||
}
|
||||
|
||||
bool SignalHandlers::remove(int handler_id)
|
||||
{
|
||||
VERIFY(handler_id != 0);
|
||||
if (m_calling_handlers) {
|
||||
auto it = m_handlers.find(handler_id);
|
||||
if (it != m_handlers.end()) {
|
||||
// Mark pending remove
|
||||
m_handlers_pending.set(handler_id, {});
|
||||
return true;
|
||||
}
|
||||
it = m_handlers_pending.find(handler_id);
|
||||
if (it != m_handlers_pending.end()) {
|
||||
if (!it->value)
|
||||
return false; // already was marked as deleted
|
||||
it->value = nullptr;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return m_handlers.remove(handler_id);
|
||||
}
|
||||
|
||||
static void post_application_event()
|
||||
{
|
||||
auto* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
|
||||
location:NSMakePoint(0, 0)
|
||||
modifierFlags:0
|
||||
timestamp:0
|
||||
windowNumber:0
|
||||
context:nil
|
||||
subtype:0
|
||||
data1:0
|
||||
data2:0];
|
||||
|
||||
[NSApp postEvent:event atStart:NO];
|
||||
}
|
||||
|
||||
NonnullOwnPtr<Core::EventLoopImplementation> CFEventLoopManager::make_implementation()
|
||||
{
|
||||
return CFEventLoopImplementation::create();
|
||||
}
|
||||
|
||||
intptr_t CFEventLoopManager::register_timer(Core::EventReceiver& receiver, int interval_milliseconds, bool should_reload, Core::TimerShouldFireWhenNotVisible should_fire_when_not_visible)
|
||||
{
|
||||
auto& thread_data = ThreadData::the();
|
||||
|
||||
auto timer_id = thread_data.timer_id_allocator.allocate();
|
||||
auto weak_receiver = receiver.make_weak_ptr();
|
||||
|
||||
auto interval_seconds = static_cast<double>(interval_milliseconds) / 1000.0;
|
||||
auto first_fire_time = CFAbsoluteTimeGetCurrent() + interval_seconds;
|
||||
|
||||
auto* timer = CFRunLoopTimerCreateWithHandler(
|
||||
kCFAllocatorDefault, first_fire_time, should_reload ? interval_seconds : 0, 0, 0,
|
||||
^(CFRunLoopTimerRef) {
|
||||
auto receiver = weak_receiver.strong_ref();
|
||||
if (!receiver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (should_fire_when_not_visible == Core::TimerShouldFireWhenNotVisible::No) {
|
||||
if (!receiver->is_visible_for_timer_purposes()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Core::TimerEvent event;
|
||||
receiver->dispatch_event(event);
|
||||
});
|
||||
|
||||
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
|
||||
thread_data.timers.set(timer_id, timer);
|
||||
|
||||
return timer_id;
|
||||
}
|
||||
|
||||
void CFEventLoopManager::unregister_timer(intptr_t timer_id)
|
||||
{
|
||||
auto& thread_data = ThreadData::the();
|
||||
thread_data.timer_id_allocator.deallocate(static_cast<int>(timer_id));
|
||||
|
||||
auto timer = thread_data.timers.take(static_cast<int>(timer_id));
|
||||
VERIFY(timer.has_value());
|
||||
CFRunLoopTimerInvalidate(*timer);
|
||||
CFRelease(*timer);
|
||||
}
|
||||
|
||||
static void socket_notifier(CFSocketRef socket, CFSocketCallBackType notification_type, CFDataRef, void const*, void*)
|
||||
{
|
||||
auto& notifier = ThreadData::the().notifier_by_fd(CFSocketGetNative(socket));
|
||||
|
||||
// This socket callback is not quite re-entrant. If Core::Notifier::dispatch_event blocks, e.g.
|
||||
// to wait upon a Core::Promise, this socket will not receive any more notifications until that
|
||||
// promise is resolved or rejected. So we mark this socket as able to receive more notifications
|
||||
// before dispatching the event, which allows it to be triggered again.
|
||||
CFSocketEnableCallBacks(socket, notification_type);
|
||||
|
||||
Core::NotifierActivationEvent event(notifier.fd(), notifier.type());
|
||||
notifier.dispatch_event(event);
|
||||
|
||||
// This manual process of enabling the callbacks also seems to require waking the event loop,
|
||||
// otherwise it hangs indefinitely in any ongoing pump(PumpMode::WaitForEvents) invocation.
|
||||
post_application_event();
|
||||
}
|
||||
|
||||
void CFEventLoopManager::register_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
auto notification_type = kCFSocketNoCallBack;
|
||||
|
||||
switch (notifier.type()) {
|
||||
case Core::Notifier::Type::Read:
|
||||
notification_type = kCFSocketReadCallBack;
|
||||
break;
|
||||
case Core::Notifier::Type::Write:
|
||||
notification_type = kCFSocketWriteCallBack;
|
||||
break;
|
||||
default:
|
||||
TODO();
|
||||
break;
|
||||
}
|
||||
|
||||
CFSocketContext context { .version = 0, .info = nullptr, .retain = nullptr, .release = nullptr, .copyDescription = nullptr };
|
||||
auto* socket = CFSocketCreateWithNative(kCFAllocatorDefault, notifier.fd(), notification_type, &socket_notifier, &context);
|
||||
|
||||
CFOptionFlags sockopt = CFSocketGetSocketFlags(socket);
|
||||
sockopt &= ~kCFSocketAutomaticallyReenableReadCallBack;
|
||||
sockopt &= ~kCFSocketCloseOnInvalidate;
|
||||
CFSocketSetSocketFlags(socket, sockopt);
|
||||
|
||||
auto* source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
|
||||
|
||||
CFRelease(socket);
|
||||
|
||||
ThreadData::the().notifiers.set(¬ifier, source);
|
||||
}
|
||||
|
||||
void CFEventLoopManager::unregister_notifier(Core::Notifier& notifier)
|
||||
{
|
||||
if (auto source = ThreadData::the().notifiers.take(¬ifier); source.has_value()) {
|
||||
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), *source, kCFRunLoopCommonModes);
|
||||
CFRelease(*source);
|
||||
}
|
||||
}
|
||||
|
||||
void CFEventLoopManager::did_post_event()
|
||||
{
|
||||
post_application_event();
|
||||
}
|
||||
|
||||
static void handle_signal(CFFileDescriptorRef f, CFOptionFlags callback_types, void* info)
|
||||
{
|
||||
VERIFY(callback_types & kCFFileDescriptorReadCallBack);
|
||||
auto* signal_handlers = static_cast<SignalHandlers*>(info);
|
||||
|
||||
struct kevent event { };
|
||||
|
||||
// returns number of events that have occurred since last call
|
||||
(void)::kevent(CFFileDescriptorGetNativeDescriptor(f), nullptr, 0, &event, 1, nullptr);
|
||||
CFFileDescriptorEnableCallBacks(f, kCFFileDescriptorReadCallBack);
|
||||
|
||||
signal_handlers->dispatch();
|
||||
}
|
||||
|
||||
int CFEventLoopManager::register_signal(int signal_number, Function<void(int)> handler)
|
||||
{
|
||||
VERIFY(signal_number != 0);
|
||||
auto& info = *signals_info();
|
||||
auto handlers = info.signal_handlers.find(signal_number);
|
||||
if (handlers == info.signal_handlers.end()) {
|
||||
auto signal_handlers = adopt_ref(*new SignalHandlers(signal_number, &handle_signal));
|
||||
auto handler_id = signal_handlers->add(move(handler));
|
||||
info.signal_handlers.set(signal_number, move(signal_handlers));
|
||||
return handler_id;
|
||||
} else {
|
||||
return handlers->value->add(move(handler));
|
||||
}
|
||||
}
|
||||
|
||||
void CFEventLoopManager::unregister_signal(int handler_id)
|
||||
{
|
||||
VERIFY(handler_id != 0);
|
||||
int remove_signal_number = 0;
|
||||
auto& info = *signals_info();
|
||||
for (auto& h : info.signal_handlers) {
|
||||
auto& handlers = *h.value;
|
||||
if (handlers.remove(handler_id)) {
|
||||
if (handlers.is_empty())
|
||||
remove_signal_number = handlers.m_signal_number;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (remove_signal_number != 0)
|
||||
info.signal_handlers.remove(remove_signal_number);
|
||||
}
|
||||
|
||||
NonnullOwnPtr<CFEventLoopImplementation> CFEventLoopImplementation::create()
|
||||
{
|
||||
return adopt_own(*new CFEventLoopImplementation);
|
||||
}
|
||||
|
||||
int CFEventLoopImplementation::exec()
|
||||
{
|
||||
[NSApp run];
|
||||
return m_exit_code;
|
||||
}
|
||||
|
||||
size_t CFEventLoopImplementation::pump(PumpMode mode)
|
||||
{
|
||||
auto* wait_until = mode == PumpMode::WaitForEvents ? [NSDate distantFuture] : [NSDate distantPast];
|
||||
|
||||
auto* event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
||||
untilDate:wait_until
|
||||
inMode:NSDefaultRunLoopMode
|
||||
dequeue:YES];
|
||||
|
||||
while (event) {
|
||||
[NSApp sendEvent:event];
|
||||
|
||||
event = [NSApp nextEventMatchingMask:NSEventMaskAny
|
||||
untilDate:nil
|
||||
inMode:NSDefaultRunLoopMode
|
||||
dequeue:YES];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void CFEventLoopImplementation::quit(int exit_code)
|
||||
{
|
||||
m_exit_code = exit_code;
|
||||
[NSApp stop:nil];
|
||||
}
|
||||
|
||||
void CFEventLoopImplementation::wake()
|
||||
{
|
||||
CFRunLoopWakeUp(CFRunLoopGetCurrent());
|
||||
}
|
||||
|
||||
void CFEventLoopImplementation::post_event(Core::EventReceiver& receiver, NonnullOwnPtr<Core::Event>&& event)
|
||||
{
|
||||
m_thread_event_queue.post_event(receiver, move(event));
|
||||
|
||||
if (&m_thread_event_queue != &Core::ThreadEventQueue::current())
|
||||
wake();
|
||||
}
|
||||
|
||||
}
|
||||
51
UI/AppKit/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
add_library(ladybird_impl STATIC
|
||||
${LADYBIRD_SOURCES}
|
||||
Application/Application.mm
|
||||
Application/ApplicationDelegate.mm
|
||||
Application/EventLoopImplementation.mm
|
||||
Interface/Event.mm
|
||||
Interface/Inspector.mm
|
||||
Interface/InspectorController.mm
|
||||
Interface/LadybirdWebView.mm
|
||||
Interface/LadybirdWebViewBridge.cpp
|
||||
Interface/LadybirdWebViewWindow.mm
|
||||
Interface/Palette.mm
|
||||
Interface/SearchPanel.mm
|
||||
Interface/Tab.mm
|
||||
Interface/TabController.mm
|
||||
Utilities/Conversions.mm
|
||||
)
|
||||
target_include_directories(ladybird_impl PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>)
|
||||
|
||||
target_compile_options(ladybird_impl PUBLIC
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fobjc-arc>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Wno-deprecated-anon-enum-enum-conversion> # Required for CGImageCreate
|
||||
)
|
||||
target_compile_features(ladybird_impl PUBLIC cxx_std_23)
|
||||
|
||||
if (ENABLE_SWIFT)
|
||||
target_sources(ladybird_impl PRIVATE
|
||||
Interface/TaskManager.swift
|
||||
Interface/TaskManagerController.swift
|
||||
)
|
||||
target_compile_definitions(ladybird_impl PUBLIC LADYBIRD_USE_SWIFT)
|
||||
set_target_properties(ladybird_impl PROPERTIES Swift_MODULE_NAME "SwiftLadybird")
|
||||
|
||||
get_target_property(LADYBIRD_NATIVE_DIRS ladybird_impl INCLUDE_DIRECTORIES)
|
||||
_swift_generate_cxx_header(ladybird_impl "Ladybird-Swift.h"
|
||||
SEARCH_PATHS ${LADYBIRD_NATIVE_DIRS}
|
||||
)
|
||||
else()
|
||||
target_sources(ladybird_impl PRIVATE
|
||||
Interface/TaskManager.mm
|
||||
Interface/TaskManagerController.mm
|
||||
)
|
||||
endif()
|
||||
|
||||
add_executable(ladybird MACOSX_BUNDLE
|
||||
main.mm
|
||||
)
|
||||
target_link_libraries(ladybird_impl PUBLIC "-framework Cocoa -framework UniformTypeIdentifiers" LibUnicode)
|
||||
target_link_libraries(ladybird PRIVATE ladybird_impl)
|
||||
|
||||
create_ladybird_bundle(ladybird)
|
||||
28
UI/AppKit/Interface/Event.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibURL/Forward.h>
|
||||
#include <LibWeb/Page/InputEvent.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type, NSEvent*, NSView*, Web::UIEvents::MouseButton);
|
||||
|
||||
Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type, id<NSDraggingInfo>, NSView*);
|
||||
Vector<URL::URL> drag_event_url_list(Web::DragEvent const&);
|
||||
|
||||
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*);
|
||||
NSEvent* key_event_to_ns_event(Web::KeyEvent const&);
|
||||
|
||||
NSEvent* create_context_menu_mouse_event(NSView*, Gfx::IntPoint);
|
||||
NSEvent* create_context_menu_mouse_event(NSView*, NSPoint);
|
||||
|
||||
}
|
||||
329
UI/AppKit/Interface/Event.mm
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWeb/HTML/SelectedFile.h>
|
||||
#include <LibWeb/UIEvents/KeyCode.h>
|
||||
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <Interface/Event.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
static Web::UIEvents::KeyModifier ns_modifiers_to_key_modifiers(NSEventModifierFlags modifier_flags, Optional<Web::UIEvents::MouseButton&> button = {})
|
||||
{
|
||||
unsigned modifiers = Web::UIEvents::KeyModifier::Mod_None;
|
||||
|
||||
if ((modifier_flags & NSEventModifierFlagShift) != 0) {
|
||||
modifiers |= Web::UIEvents::KeyModifier::Mod_Shift;
|
||||
}
|
||||
if ((modifier_flags & NSEventModifierFlagControl) != 0) {
|
||||
if (button == Web::UIEvents::MouseButton::Primary) {
|
||||
*button = Web::UIEvents::MouseButton::Secondary;
|
||||
} else {
|
||||
modifiers |= Web::UIEvents::KeyModifier::Mod_Ctrl;
|
||||
}
|
||||
}
|
||||
if ((modifier_flags & NSEventModifierFlagOption) != 0) {
|
||||
modifiers |= Web::UIEvents::KeyModifier::Mod_Alt;
|
||||
}
|
||||
if ((modifier_flags & NSEventModifierFlagCommand) != 0) {
|
||||
modifiers |= Web::UIEvents::KeyModifier::Mod_Super;
|
||||
}
|
||||
|
||||
return static_cast<Web::UIEvents::KeyModifier>(modifiers);
|
||||
}
|
||||
|
||||
Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type type, NSEvent* event, NSView* view, Web::UIEvents::MouseButton button)
|
||||
{
|
||||
auto position = [view convertPoint:event.locationInWindow fromView:nil];
|
||||
auto device_position = ns_point_to_gfx_point(position).to_type<Web::DevicePixels>();
|
||||
|
||||
auto screen_position = [NSEvent mouseLocation];
|
||||
auto device_screen_position = ns_point_to_gfx_point(screen_position).to_type<Web::DevicePixels>();
|
||||
|
||||
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags, button);
|
||||
|
||||
int wheel_delta_x = 0;
|
||||
int wheel_delta_y = 0;
|
||||
|
||||
if (type == Web::MouseEvent::Type::MouseDown) {
|
||||
if (event.clickCount % 2 == 0) {
|
||||
type = Web::MouseEvent::Type::DoubleClick;
|
||||
}
|
||||
} else if (type == Web::MouseEvent::Type::MouseWheel) {
|
||||
CGFloat delta_x = -[event scrollingDeltaX];
|
||||
CGFloat delta_y = -[event scrollingDeltaY];
|
||||
|
||||
if (![event hasPreciseScrollingDeltas]) {
|
||||
static constexpr CGFloat imprecise_scroll_multiplier = 24;
|
||||
|
||||
delta_x *= imprecise_scroll_multiplier;
|
||||
delta_y *= imprecise_scroll_multiplier;
|
||||
}
|
||||
|
||||
wheel_delta_x = static_cast<int>(delta_x);
|
||||
wheel_delta_y = static_cast<int>(delta_y);
|
||||
}
|
||||
|
||||
return { type, device_position, device_screen_position, button, button, modifiers, wheel_delta_x, wheel_delta_y, nullptr };
|
||||
}
|
||||
|
||||
struct DragData : public Web::ChromeInputData {
|
||||
explicit DragData(Vector<URL::URL> urls)
|
||||
: urls(move(urls))
|
||||
{
|
||||
}
|
||||
|
||||
Vector<URL::URL> urls;
|
||||
};
|
||||
|
||||
Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type type, id<NSDraggingInfo> event, NSView* view)
|
||||
{
|
||||
auto position = [view convertPoint:event.draggingLocation fromView:nil];
|
||||
auto device_position = ns_point_to_gfx_point(position).to_type<Web::DevicePixels>();
|
||||
|
||||
auto screen_position = [NSEvent mouseLocation];
|
||||
auto device_screen_position = ns_point_to_gfx_point(screen_position).to_type<Web::DevicePixels>();
|
||||
|
||||
auto button = Web::UIEvents::MouseButton::Primary;
|
||||
auto modifiers = ns_modifiers_to_key_modifiers([NSEvent modifierFlags], button);
|
||||
|
||||
Vector<Web::HTML::SelectedFile> files;
|
||||
OwnPtr<DragData> chrome_data;
|
||||
|
||||
auto for_each_file = [&](auto callback) {
|
||||
NSArray* file_list = [[event draggingPasteboard] readObjectsForClasses:@[ [NSURL class] ]
|
||||
options:nil];
|
||||
|
||||
for (NSURL* file in file_list) {
|
||||
auto file_path = Ladybird::ns_string_to_byte_string([file path]);
|
||||
callback(file_path);
|
||||
}
|
||||
};
|
||||
|
||||
if (type == Web::DragEvent::Type::DragStart) {
|
||||
for_each_file([&](ByteString const& file_path) {
|
||||
if (auto file = Web::HTML::SelectedFile::from_file_path(file_path); file.is_error())
|
||||
warnln("Unable to open file {}: {}", file_path, file.error());
|
||||
else
|
||||
files.append(file.release_value());
|
||||
});
|
||||
} else if (type == Web::DragEvent::Type::Drop) {
|
||||
Vector<URL::URL> urls;
|
||||
|
||||
for_each_file([&](ByteString const& file_path) {
|
||||
if (auto url = URL::create_with_url_or_path(file_path); url.is_valid())
|
||||
urls.append(move(url));
|
||||
});
|
||||
|
||||
chrome_data = make<DragData>(move(urls));
|
||||
}
|
||||
|
||||
return { type, device_position, device_screen_position, button, button, modifiers, move(files), move(chrome_data) };
|
||||
}
|
||||
|
||||
Vector<URL::URL> drag_event_url_list(Web::DragEvent const& event)
|
||||
{
|
||||
auto& chrome_data = verify_cast<DragData>(*event.chrome_data);
|
||||
return move(chrome_data.urls);
|
||||
}
|
||||
|
||||
NSEvent* create_context_menu_mouse_event(NSView* view, Gfx::IntPoint position)
|
||||
{
|
||||
return create_context_menu_mouse_event(view, gfx_point_to_ns_point(position));
|
||||
}
|
||||
|
||||
NSEvent* create_context_menu_mouse_event(NSView* view, NSPoint position)
|
||||
{
|
||||
return [NSEvent mouseEventWithType:NSEventTypeRightMouseUp
|
||||
location:[view convertPoint:position fromView:nil]
|
||||
modifierFlags:0
|
||||
timestamp:0
|
||||
windowNumber:[[view window] windowNumber]
|
||||
context:nil
|
||||
eventNumber:1
|
||||
clickCount:1
|
||||
pressure:1.0];
|
||||
}
|
||||
|
||||
static Web::UIEvents::KeyCode ns_key_code_to_key_code(unsigned short key_code, Web::UIEvents::KeyModifier& modifiers)
|
||||
{
|
||||
auto augment_modifiers_and_return = [&](auto key, auto modifier) {
|
||||
modifiers = static_cast<Web::UIEvents::KeyModifier>(static_cast<unsigned>(modifiers) | modifier);
|
||||
return key;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
switch (key_code) {
|
||||
case kVK_ANSI_0: return Web::UIEvents::KeyCode::Key_0;
|
||||
case kVK_ANSI_1: return Web::UIEvents::KeyCode::Key_1;
|
||||
case kVK_ANSI_2: return Web::UIEvents::KeyCode::Key_2;
|
||||
case kVK_ANSI_3: return Web::UIEvents::KeyCode::Key_3;
|
||||
case kVK_ANSI_4: return Web::UIEvents::KeyCode::Key_4;
|
||||
case kVK_ANSI_5: return Web::UIEvents::KeyCode::Key_5;
|
||||
case kVK_ANSI_6: return Web::UIEvents::KeyCode::Key_6;
|
||||
case kVK_ANSI_7: return Web::UIEvents::KeyCode::Key_7;
|
||||
case kVK_ANSI_8: return Web::UIEvents::KeyCode::Key_8;
|
||||
case kVK_ANSI_9: return Web::UIEvents::KeyCode::Key_9;
|
||||
case kVK_ANSI_A: return Web::UIEvents::KeyCode::Key_A;
|
||||
case kVK_ANSI_B: return Web::UIEvents::KeyCode::Key_B;
|
||||
case kVK_ANSI_C: return Web::UIEvents::KeyCode::Key_C;
|
||||
case kVK_ANSI_D: return Web::UIEvents::KeyCode::Key_D;
|
||||
case kVK_ANSI_E: return Web::UIEvents::KeyCode::Key_E;
|
||||
case kVK_ANSI_F: return Web::UIEvents::KeyCode::Key_F;
|
||||
case kVK_ANSI_G: return Web::UIEvents::KeyCode::Key_G;
|
||||
case kVK_ANSI_H: return Web::UIEvents::KeyCode::Key_H;
|
||||
case kVK_ANSI_I: return Web::UIEvents::KeyCode::Key_I;
|
||||
case kVK_ANSI_J: return Web::UIEvents::KeyCode::Key_J;
|
||||
case kVK_ANSI_K: return Web::UIEvents::KeyCode::Key_K;
|
||||
case kVK_ANSI_L: return Web::UIEvents::KeyCode::Key_L;
|
||||
case kVK_ANSI_M: return Web::UIEvents::KeyCode::Key_M;
|
||||
case kVK_ANSI_N: return Web::UIEvents::KeyCode::Key_N;
|
||||
case kVK_ANSI_O: return Web::UIEvents::KeyCode::Key_O;
|
||||
case kVK_ANSI_P: return Web::UIEvents::KeyCode::Key_P;
|
||||
case kVK_ANSI_Q: return Web::UIEvents::KeyCode::Key_Q;
|
||||
case kVK_ANSI_R: return Web::UIEvents::KeyCode::Key_R;
|
||||
case kVK_ANSI_S: return Web::UIEvents::KeyCode::Key_S;
|
||||
case kVK_ANSI_T: return Web::UIEvents::KeyCode::Key_T;
|
||||
case kVK_ANSI_U: return Web::UIEvents::KeyCode::Key_U;
|
||||
case kVK_ANSI_V: return Web::UIEvents::KeyCode::Key_V;
|
||||
case kVK_ANSI_W: return Web::UIEvents::KeyCode::Key_W;
|
||||
case kVK_ANSI_X: return Web::UIEvents::KeyCode::Key_X;
|
||||
case kVK_ANSI_Y: return Web::UIEvents::KeyCode::Key_Y;
|
||||
case kVK_ANSI_Z: return Web::UIEvents::KeyCode::Key_Z;
|
||||
case kVK_ANSI_Backslash: return Web::UIEvents::KeyCode::Key_Backslash;
|
||||
case kVK_ANSI_Comma: return Web::UIEvents::KeyCode::Key_Comma;
|
||||
case kVK_ANSI_Equal: return Web::UIEvents::KeyCode::Key_Equal;
|
||||
case kVK_ANSI_Grave: return Web::UIEvents::KeyCode::Key_Backtick;
|
||||
case kVK_ANSI_Keypad0: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_0, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad1: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_1, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad2: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_2, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad3: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_3, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad4: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_4, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad5: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_5, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad6: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_6, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad7: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_7, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad8: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_8, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_Keypad9: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_9, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadClear: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Delete, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadDecimal: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Period, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadDivide: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Slash, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadEnter: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Return, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadEquals: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Equal, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadMinus: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Minus, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadMultiply: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Asterisk, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_KeypadPlus: return augment_modifiers_and_return(Web::UIEvents::KeyCode::Key_Plus, Web::UIEvents::KeyModifier::Mod_Keypad);
|
||||
case kVK_ANSI_LeftBracket: return Web::UIEvents::KeyCode::Key_LeftBracket;
|
||||
case kVK_ANSI_Minus: return Web::UIEvents::KeyCode::Key_Minus;
|
||||
case kVK_ANSI_Period: return Web::UIEvents::KeyCode::Key_Period;
|
||||
case kVK_ANSI_Quote: return Web::UIEvents::KeyCode::Key_Apostrophe;
|
||||
case kVK_ANSI_RightBracket: return Web::UIEvents::KeyCode::Key_RightBracket;
|
||||
case kVK_ANSI_Semicolon: return Web::UIEvents::KeyCode::Key_Semicolon;
|
||||
case kVK_ANSI_Slash: return Web::UIEvents::KeyCode::Key_Slash;
|
||||
case kVK_CapsLock: return Web::UIEvents::KeyCode::Key_CapsLock;
|
||||
case kVK_Command: return Web::UIEvents::KeyCode::Key_LeftSuper;
|
||||
case kVK_Control: return Web::UIEvents::KeyCode::Key_LeftControl;
|
||||
case kVK_Delete: return Web::UIEvents::KeyCode::Key_Backspace;
|
||||
case kVK_DownArrow: return Web::UIEvents::KeyCode::Key_Down;
|
||||
case kVK_End: return Web::UIEvents::KeyCode::Key_End;
|
||||
case kVK_Escape: return Web::UIEvents::KeyCode::Key_Escape;
|
||||
case kVK_F1: return Web::UIEvents::KeyCode::Key_F1;
|
||||
case kVK_F2: return Web::UIEvents::KeyCode::Key_F2;
|
||||
case kVK_F3: return Web::UIEvents::KeyCode::Key_F3;
|
||||
case kVK_F4: return Web::UIEvents::KeyCode::Key_F4;
|
||||
case kVK_F5: return Web::UIEvents::KeyCode::Key_F5;
|
||||
case kVK_F6: return Web::UIEvents::KeyCode::Key_F6;
|
||||
case kVK_F7: return Web::UIEvents::KeyCode::Key_F7;
|
||||
case kVK_F8: return Web::UIEvents::KeyCode::Key_F8;
|
||||
case kVK_F9: return Web::UIEvents::KeyCode::Key_F9;
|
||||
case kVK_F10: return Web::UIEvents::KeyCode::Key_F10;
|
||||
case kVK_F11: return Web::UIEvents::KeyCode::Key_F11;
|
||||
case kVK_F12: return Web::UIEvents::KeyCode::Key_F12;
|
||||
case kVK_ForwardDelete: return Web::UIEvents::KeyCode::Key_Delete;
|
||||
case kVK_Home: return Web::UIEvents::KeyCode::Key_Home;
|
||||
case kVK_LeftArrow: return Web::UIEvents::KeyCode::Key_Left;
|
||||
case kVK_Option: return Web::UIEvents::KeyCode::Key_LeftAlt;
|
||||
case kVK_PageDown: return Web::UIEvents::KeyCode::Key_PageDown;
|
||||
case kVK_PageUp: return Web::UIEvents::KeyCode::Key_PageUp;
|
||||
case kVK_Return: return Web::UIEvents::KeyCode::Key_Return;
|
||||
case kVK_RightArrow: return Web::UIEvents::KeyCode::Key_Right;
|
||||
case kVK_RightCommand: return Web::UIEvents::KeyCode::Key_RightSuper;
|
||||
case kVK_RightControl: return Web::UIEvents::KeyCode::Key_RightControl;
|
||||
case kVK_RightOption: return Web::UIEvents::KeyCode::Key_RightAlt;
|
||||
case kVK_RightShift: return Web::UIEvents::KeyCode::Key_RightShift;
|
||||
case kVK_Shift: return Web::UIEvents::KeyCode::Key_LeftShift;
|
||||
case kVK_Space: return Web::UIEvents::KeyCode::Key_Space;
|
||||
case kVK_Tab: return Web::UIEvents::KeyCode::Key_Tab;
|
||||
case kVK_UpArrow: return Web::UIEvents::KeyCode::Key_Up;
|
||||
default: break;
|
||||
}
|
||||
// clang-format on
|
||||
|
||||
return Web::UIEvents::KeyCode::Key_Invalid;
|
||||
}
|
||||
|
||||
class KeyData : public Web::ChromeInputData {
|
||||
public:
|
||||
explicit KeyData(NSEvent* event)
|
||||
: m_event(CFBridgingRetain(event))
|
||||
{
|
||||
}
|
||||
|
||||
virtual ~KeyData() override
|
||||
{
|
||||
if (m_event != nullptr) {
|
||||
CFBridgingRelease(m_event);
|
||||
}
|
||||
}
|
||||
|
||||
NSEvent* take_event()
|
||||
{
|
||||
VERIFY(m_event != nullptr);
|
||||
|
||||
CFTypeRef event = exchange(m_event, nullptr);
|
||||
return CFBridgingRelease(event);
|
||||
}
|
||||
|
||||
private:
|
||||
CFTypeRef m_event { nullptr };
|
||||
};
|
||||
|
||||
Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type type, NSEvent* event)
|
||||
{
|
||||
auto modifiers = ns_modifiers_to_key_modifiers(event.modifierFlags);
|
||||
auto key_code = ns_key_code_to_key_code(event.keyCode, modifiers);
|
||||
auto repeat = false;
|
||||
|
||||
// FIXME: WebContent should really support multi-code point key events.
|
||||
u32 code_point = 0;
|
||||
|
||||
if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) {
|
||||
auto const* utf8 = [event.characters UTF8String];
|
||||
Utf8View utf8_view { StringView { utf8, strlen(utf8) } };
|
||||
|
||||
code_point = utf8_view.is_empty() ? 0u : *utf8_view.begin();
|
||||
|
||||
repeat = event.isARepeat;
|
||||
}
|
||||
|
||||
// NSEvent assigns PUA code points to to functional keys, e.g. arrow keys. Do not propagate them.
|
||||
if (code_point >= 0xE000 && code_point <= 0xF8FF)
|
||||
code_point = 0;
|
||||
|
||||
return { type, key_code, modifiers, code_point, repeat, make<KeyData>(event) };
|
||||
}
|
||||
|
||||
NSEvent* key_event_to_ns_event(Web::KeyEvent const& event)
|
||||
{
|
||||
auto& chrome_data = verify_cast<KeyData>(*event.chrome_data);
|
||||
return chrome_data.take_event();
|
||||
}
|
||||
|
||||
}
|
||||
24
UI/AppKit/Interface/Inspector.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Interface/LadybirdWebViewWindow.h>
|
||||
|
||||
@class LadybirdWebView;
|
||||
@class Tab;
|
||||
|
||||
@interface Inspector : LadybirdWebViewWindow
|
||||
|
||||
- (instancetype)init:(Tab*)tab;
|
||||
|
||||
- (void)inspect;
|
||||
- (void)reset;
|
||||
|
||||
- (void)selectHoveredElement;
|
||||
|
||||
@end
|
||||
372
UI/AppKit/Interface/Inspector.mm
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Cookie/Cookie.h>
|
||||
#include <LibWebView/Attribute.h>
|
||||
#include <LibWebView/InspectorClient.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
|
||||
#import <Interface/Event.h>
|
||||
#import <Interface/Inspector.h>
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
static constexpr CGFloat const WINDOW_WIDTH = 875;
|
||||
static constexpr CGFloat const WINDOW_HEIGHT = 825;
|
||||
|
||||
static constexpr NSInteger CONTEXT_MENU_EDIT_NODE_TAG = 1;
|
||||
static constexpr NSInteger CONTEXT_MENU_REMOVE_ATTRIBUTE_TAG = 2;
|
||||
static constexpr NSInteger CONTEXT_MENU_COPY_ATTRIBUTE_VALUE_TAG = 3;
|
||||
static constexpr NSInteger CONTEXT_MENU_DELETE_COOKIE_TAG = 4;
|
||||
|
||||
@interface Inspector ()
|
||||
{
|
||||
OwnPtr<WebView::InspectorClient> m_inspector_client;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) Tab* tab;
|
||||
|
||||
@property (nonatomic, strong) NSMenu* dom_node_text_context_menu;
|
||||
@property (nonatomic, strong) NSMenu* dom_node_tag_context_menu;
|
||||
@property (nonatomic, strong) NSMenu* dom_node_attribute_context_menu;
|
||||
@property (nonatomic, strong) NSMenu* cookie_context_menu;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Inspector
|
||||
|
||||
@synthesize tab = _tab;
|
||||
@synthesize dom_node_text_context_menu = _dom_node_text_context_menu;
|
||||
@synthesize dom_node_tag_context_menu = _dom_node_tag_context_menu;
|
||||
@synthesize dom_node_attribute_context_menu = _dom_node_attribute_context_menu;
|
||||
@synthesize cookie_context_menu = _cookie_context_menu;
|
||||
|
||||
- (instancetype)init:(Tab*)tab
|
||||
{
|
||||
auto tab_rect = [tab frame];
|
||||
auto position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2;
|
||||
auto position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2;
|
||||
auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
|
||||
if (self = [super initWithWebView:nil windowRect:window_rect]) {
|
||||
self.tab = tab;
|
||||
|
||||
m_inspector_client = make<WebView::InspectorClient>([[tab web_view] view], [[self web_view] view]);
|
||||
__weak Inspector* weak_self = self;
|
||||
|
||||
m_inspector_client->on_requested_dom_node_text_context_menu = [weak_self](auto position) {
|
||||
Inspector* strong_self = weak_self;
|
||||
if (strong_self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
|
||||
[NSMenu popUpContextMenu:strong_self.dom_node_text_context_menu withEvent:event forView:strong_self.web_view];
|
||||
};
|
||||
|
||||
m_inspector_client->on_requested_dom_node_tag_context_menu = [weak_self](auto position, auto const& tag) {
|
||||
Inspector* strong_self = weak_self;
|
||||
if (strong_self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto edit_node_text = MUST(String::formatted("Edit \"{}\"", tag));
|
||||
|
||||
auto* edit_node_menu_item = [strong_self.dom_node_tag_context_menu itemWithTag:CONTEXT_MENU_EDIT_NODE_TAG];
|
||||
[edit_node_menu_item setTitle:Ladybird::string_to_ns_string(edit_node_text)];
|
||||
|
||||
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
|
||||
[NSMenu popUpContextMenu:strong_self.dom_node_tag_context_menu withEvent:event forView:strong_self.web_view];
|
||||
};
|
||||
|
||||
m_inspector_client->on_requested_dom_node_attribute_context_menu = [weak_self](auto position, auto const&, auto const& attribute) {
|
||||
Inspector* strong_self = weak_self;
|
||||
if (strong_self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr size_t MAX_ATTRIBUTE_VALUE_LENGTH = 32;
|
||||
|
||||
auto edit_attribute_text = MUST(String::formatted("Edit attribute \"{}\"", attribute.name));
|
||||
auto remove_attribute_text = MUST(String::formatted("Remove attribute \"{}\"", attribute.name));
|
||||
auto copy_attribute_value_text = MUST(String::formatted("Copy attribute value \"{:.{}}{}\"",
|
||||
attribute.value, MAX_ATTRIBUTE_VALUE_LENGTH,
|
||||
attribute.value.bytes_as_string_view().length() > MAX_ATTRIBUTE_VALUE_LENGTH ? "..."sv : ""sv));
|
||||
|
||||
auto* edit_node_menu_item = [strong_self.dom_node_attribute_context_menu itemWithTag:CONTEXT_MENU_EDIT_NODE_TAG];
|
||||
[edit_node_menu_item setTitle:Ladybird::string_to_ns_string(edit_attribute_text)];
|
||||
|
||||
auto* remove_attribute_menu_item = [strong_self.dom_node_attribute_context_menu itemWithTag:CONTEXT_MENU_REMOVE_ATTRIBUTE_TAG];
|
||||
[remove_attribute_menu_item setTitle:Ladybird::string_to_ns_string(remove_attribute_text)];
|
||||
|
||||
auto* copy_attribute_value_menu_item = [strong_self.dom_node_attribute_context_menu itemWithTag:CONTEXT_MENU_COPY_ATTRIBUTE_VALUE_TAG];
|
||||
[copy_attribute_value_menu_item setTitle:Ladybird::string_to_ns_string(copy_attribute_value_text)];
|
||||
|
||||
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
|
||||
[NSMenu popUpContextMenu:strong_self.dom_node_attribute_context_menu withEvent:event forView:strong_self.web_view];
|
||||
};
|
||||
|
||||
m_inspector_client->on_requested_cookie_context_menu = [weak_self](auto position, auto const& cookie) {
|
||||
Inspector* strong_self = weak_self;
|
||||
if (strong_self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto delete_cookie_text = MUST(String::formatted("Delete \"{}\"", cookie.name));
|
||||
|
||||
auto* delete_cookie_item = [strong_self.cookie_context_menu itemWithTag:CONTEXT_MENU_DELETE_COOKIE_TAG];
|
||||
[delete_cookie_item setTitle:Ladybird::string_to_ns_string(delete_cookie_text)];
|
||||
|
||||
auto* event = Ladybird::create_context_menu_mouse_event(strong_self.web_view, position);
|
||||
[NSMenu popUpContextMenu:strong_self.cookie_context_menu withEvent:event forView:strong_self.web_view];
|
||||
};
|
||||
|
||||
[self setContentView:self.web_view];
|
||||
[self setTitle:@"Inspector"];
|
||||
[self setIsVisible:YES];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
auto& web_view = [[self.tab web_view] view];
|
||||
web_view.clear_inspected_dom_node();
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)inspect
|
||||
{
|
||||
m_inspector_client->inspect();
|
||||
}
|
||||
|
||||
- (void)reset
|
||||
{
|
||||
m_inspector_client->reset();
|
||||
}
|
||||
|
||||
- (void)selectHoveredElement
|
||||
{
|
||||
m_inspector_client->select_hovered_node();
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (void)editDOMNode:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_edit_dom_node();
|
||||
}
|
||||
|
||||
- (void)copyDOMNode:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_copy_dom_node();
|
||||
}
|
||||
|
||||
- (void)screenshotDOMNode:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_screenshot_dom_node();
|
||||
}
|
||||
|
||||
- (void)createChildElement:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_create_child_element();
|
||||
}
|
||||
|
||||
- (void)createChildTextNode:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_create_child_text_node();
|
||||
}
|
||||
|
||||
- (void)cloneDOMNode:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_clone_dom_node();
|
||||
}
|
||||
|
||||
- (void)deleteDOMNode:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_remove_dom_node();
|
||||
}
|
||||
|
||||
- (void)addDOMAttribute:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_add_dom_node_attribute();
|
||||
}
|
||||
|
||||
- (void)removeDOMAttribute:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_remove_dom_node_attribute();
|
||||
}
|
||||
|
||||
- (void)copyDOMAttributeValue:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_copy_dom_node_attribute_value();
|
||||
}
|
||||
|
||||
- (void)deleteCookie:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_delete_cookie();
|
||||
}
|
||||
|
||||
- (void)deleteAllCookies:(id)sender
|
||||
{
|
||||
m_inspector_client->context_menu_delete_all_cookies();
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
+ (NSMenuItem*)make_create_child_menu
|
||||
{
|
||||
auto* create_child_menu = [[NSMenu alloc] init];
|
||||
[create_child_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Create child element"
|
||||
action:@selector(createChildElement:)
|
||||
keyEquivalent:@""]];
|
||||
[create_child_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Create child text node"
|
||||
action:@selector(createChildTextNode:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
auto* create_child_menu_item = [[NSMenuItem alloc] initWithTitle:@"Create child"
|
||||
action:nil
|
||||
keyEquivalent:@""];
|
||||
[create_child_menu_item setSubmenu:create_child_menu];
|
||||
|
||||
return create_child_menu_item;
|
||||
}
|
||||
|
||||
- (NSMenu*)dom_node_text_context_menu
|
||||
{
|
||||
if (!_dom_node_text_context_menu) {
|
||||
_dom_node_text_context_menu = [[NSMenu alloc] initWithTitle:@"DOM Text Context Menu"];
|
||||
|
||||
[_dom_node_text_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Edit text"
|
||||
action:@selector(editDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_text_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy text"
|
||||
action:@selector(copyDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
[_dom_node_text_context_menu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[_dom_node_text_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete node"
|
||||
action:@selector(deleteDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
}
|
||||
|
||||
return _dom_node_text_context_menu;
|
||||
}
|
||||
|
||||
- (NSMenu*)dom_node_tag_context_menu
|
||||
{
|
||||
if (!_dom_node_tag_context_menu) {
|
||||
_dom_node_tag_context_menu = [[NSMenu alloc] initWithTitle:@"DOM Tag Context Menu"];
|
||||
|
||||
auto* edit_node_menu_item = [[NSMenuItem alloc] initWithTitle:@"Edit tag"
|
||||
action:@selector(editDOMNode:)
|
||||
keyEquivalent:@""];
|
||||
[edit_node_menu_item setTag:CONTEXT_MENU_EDIT_NODE_TAG];
|
||||
[_dom_node_tag_context_menu addItem:edit_node_menu_item];
|
||||
|
||||
[_dom_node_tag_context_menu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Add attribute"
|
||||
action:@selector(addDOMAttribute:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_tag_context_menu addItem:[Inspector make_create_child_menu]];
|
||||
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Clone node"
|
||||
action:@selector(cloneDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete node"
|
||||
action:@selector(deleteDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
[_dom_node_tag_context_menu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy HTML"
|
||||
action:@selector(copyDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_tag_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Take node screenshot"
|
||||
action:@selector(screenshotDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
}
|
||||
|
||||
return _dom_node_tag_context_menu;
|
||||
}
|
||||
|
||||
- (NSMenu*)dom_node_attribute_context_menu
|
||||
{
|
||||
if (!_dom_node_attribute_context_menu) {
|
||||
_dom_node_attribute_context_menu = [[NSMenu alloc] initWithTitle:@"DOM Attribute Context Menu"];
|
||||
|
||||
auto* edit_node_menu_item = [[NSMenuItem alloc] initWithTitle:@"Edit attribute"
|
||||
action:@selector(editDOMNode:)
|
||||
keyEquivalent:@""];
|
||||
[edit_node_menu_item setTag:CONTEXT_MENU_EDIT_NODE_TAG];
|
||||
[_dom_node_attribute_context_menu addItem:edit_node_menu_item];
|
||||
|
||||
auto* remove_attribute_menu_item = [[NSMenuItem alloc] initWithTitle:@"Remove attribute"
|
||||
action:@selector(removeDOMAttribute:)
|
||||
keyEquivalent:@""];
|
||||
[remove_attribute_menu_item setTag:CONTEXT_MENU_REMOVE_ATTRIBUTE_TAG];
|
||||
[_dom_node_attribute_context_menu addItem:remove_attribute_menu_item];
|
||||
|
||||
auto* copy_attribute_value_menu_item = [[NSMenuItem alloc] initWithTitle:@"Copy attribute value"
|
||||
action:@selector(copyDOMAttributeValue:)
|
||||
keyEquivalent:@""];
|
||||
[copy_attribute_value_menu_item setTag:CONTEXT_MENU_COPY_ATTRIBUTE_VALUE_TAG];
|
||||
[_dom_node_attribute_context_menu addItem:copy_attribute_value_menu_item];
|
||||
|
||||
[_dom_node_attribute_context_menu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Add attribute"
|
||||
action:@selector(addDOMAttribute:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_attribute_context_menu addItem:[Inspector make_create_child_menu]];
|
||||
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Clone node"
|
||||
action:@selector(cloneDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete node"
|
||||
action:@selector(deleteDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
|
||||
[_dom_node_attribute_context_menu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Copy HTML"
|
||||
action:@selector(copyDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
[_dom_node_attribute_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Take node screenshot"
|
||||
action:@selector(screenshotDOMNode:)
|
||||
keyEquivalent:@""]];
|
||||
}
|
||||
|
||||
return _dom_node_attribute_context_menu;
|
||||
}
|
||||
|
||||
- (NSMenu*)cookie_context_menu
|
||||
{
|
||||
if (!_cookie_context_menu) {
|
||||
_cookie_context_menu = [[NSMenu alloc] initWithTitle:@"Cookie Context Menu"];
|
||||
|
||||
auto* delete_cookie_item = [[NSMenuItem alloc] initWithTitle:@"Delete cookie"
|
||||
action:@selector(deleteCookie:)
|
||||
keyEquivalent:@""];
|
||||
[delete_cookie_item setTag:CONTEXT_MENU_DELETE_COOKIE_TAG];
|
||||
[_cookie_context_menu addItem:delete_cookie_item];
|
||||
|
||||
[_cookie_context_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Delete all cookies"
|
||||
action:@selector(deleteAllCookies:)
|
||||
keyEquivalent:@""]];
|
||||
}
|
||||
|
||||
return _cookie_context_menu;
|
||||
}
|
||||
|
||||
@end
|
||||
17
UI/AppKit/Interface/InspectorController.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class Tab;
|
||||
|
||||
@interface InspectorController : NSWindowController
|
||||
|
||||
- (instancetype)init:(Tab*)tab;
|
||||
|
||||
@end
|
||||
66
UI/AppKit/Interface/InspectorController.mm
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#import <Interface/Inspector.h>
|
||||
#import <Interface/InspectorController.h>
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/Tab.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
@interface InspectorController () <NSWindowDelegate>
|
||||
|
||||
@property (nonatomic, strong) Tab* tab;
|
||||
|
||||
@end
|
||||
|
||||
@implementation InspectorController
|
||||
|
||||
- (instancetype)init:(Tab*)tab
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.tab = tab;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (Inspector*)inspector
|
||||
{
|
||||
return (Inspector*)[self window];
|
||||
}
|
||||
|
||||
#pragma mark - NSWindowController
|
||||
|
||||
- (IBAction)showWindow:(id)sender
|
||||
{
|
||||
self.window = [[Inspector alloc] init:self.tab];
|
||||
[self.window setDelegate:self];
|
||||
[self.window makeKeyAndOrderFront:sender];
|
||||
}
|
||||
|
||||
#pragma mark - NSWindowDelegate
|
||||
|
||||
- (void)windowWillClose:(NSNotification*)notification
|
||||
{
|
||||
[self.tab onInspectorClosed];
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(NSNotification*)notification
|
||||
{
|
||||
[[[self inspector] web_view] handleResize];
|
||||
}
|
||||
|
||||
- (void)windowDidChangeBackingProperties:(NSNotification*)notification
|
||||
{
|
||||
[[[self inspector] web_view] handleDevicePixelRatioChange];
|
||||
}
|
||||
|
||||
@end
|
||||
95
UI/AppKit/Interface/LadybirdWebView.h
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <LibGfx/Forward.h>
|
||||
#include <LibURL/Forward.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/CSS/PreferredContrast.h>
|
||||
#include <LibWeb/CSS/PreferredMotion.h>
|
||||
#include <LibWeb/HTML/ActivateTab.h>
|
||||
#include <LibWeb/HTML/AudioPlayState.h>
|
||||
#include <LibWebView/Forward.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@protocol LadybirdWebViewObserver <NSObject>
|
||||
|
||||
- (String const&)onCreateNewTab:(Optional<URL::URL> const&)url
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab;
|
||||
|
||||
- (String const&)onCreateNewTab:(StringView)html
|
||||
url:(URL::URL const&)url
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab;
|
||||
|
||||
- (String const&)onCreateChildTab:(Optional<URL::URL> const&)url
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
pageIndex:(u64)page_index;
|
||||
|
||||
- (void)loadURL:(URL::URL const&)url;
|
||||
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect;
|
||||
- (void)onLoadFinish:(URL::URL const&)url;
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url;
|
||||
- (void)onBackNavigationEnabled:(BOOL)back_enabled
|
||||
forwardNavigationEnabled:(BOOL)forward_enabled;
|
||||
|
||||
- (void)onTitleChange:(ByteString const&)title;
|
||||
- (void)onFaviconChange:(Gfx::Bitmap const&)bitmap;
|
||||
- (void)onAudioPlayStateChange:(Web::HTML::AudioPlayState)play_state;
|
||||
|
||||
- (void)onFindInPageResult:(size_t)current_match_index
|
||||
totalMatchCount:(Optional<size_t> const&)total_match_count;
|
||||
|
||||
@end
|
||||
|
||||
@interface LadybirdWebView : NSView <NSMenuDelegate>
|
||||
|
||||
- (instancetype)init:(id<LadybirdWebViewObserver>)observer;
|
||||
- (instancetype)initAsChild:(id<LadybirdWebViewObserver>)observer
|
||||
parent:(LadybirdWebView*)parent
|
||||
pageIndex:(u64)page_index;
|
||||
|
||||
- (void)loadURL:(URL::URL const&)url;
|
||||
- (void)loadHTML:(StringView)html;
|
||||
|
||||
- (void)navigateBack;
|
||||
- (void)navigateForward;
|
||||
- (void)reload;
|
||||
|
||||
- (WebView::ViewImplementation&)view;
|
||||
- (String const&)handle;
|
||||
|
||||
- (void)setWindowPosition:(Gfx::IntPoint)position;
|
||||
- (void)setWindowSize:(Gfx::IntSize)size;
|
||||
|
||||
- (void)handleResize;
|
||||
- (void)handleDevicePixelRatioChange;
|
||||
- (void)handleVisibility:(BOOL)is_visible;
|
||||
|
||||
- (void)setPreferredColorScheme:(Web::CSS::PreferredColorScheme)color_scheme;
|
||||
- (void)setPreferredContrast:(Web::CSS::PreferredContrast)contrast;
|
||||
- (void)setPreferredMotion:(Web::CSS::PreferredMotion)motion;
|
||||
|
||||
- (void)findInPage:(NSString*)query
|
||||
caseSensitivity:(CaseSensitivity)case_sensitivity;
|
||||
- (void)findInPageNextMatch;
|
||||
- (void)findInPagePreviousMatch;
|
||||
|
||||
- (void)zoomIn;
|
||||
- (void)zoomOut;
|
||||
- (void)resetZoom;
|
||||
- (float)zoomLevel;
|
||||
|
||||
- (void)debugRequest:(ByteString const&)request argument:(ByteString const&)argument;
|
||||
|
||||
- (void)setEnableAutoplay:(BOOL)enabled;
|
||||
|
||||
- (void)viewSource;
|
||||
|
||||
@end
|
||||
1735
UI/AppKit/Interface/LadybirdWebView.mm
Normal file
198
UI/AppKit/Interface/LadybirdWebViewBridge.cpp
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <Interface/LadybirdWebViewBridge.h>
|
||||
#include <LibGfx/Font/FontDatabase.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibIPC/File.h>
|
||||
#include <LibWeb/Crypto/Crypto.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/UserAgent.h>
|
||||
#include <UI/HelperProcess.h>
|
||||
#include <UI/Utilities.h>
|
||||
|
||||
#import <Interface/Palette.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
template<typename T>
|
||||
static T scale_for_device(T size, float device_pixel_ratio)
|
||||
{
|
||||
return size.template to_type<float>().scaled(device_pixel_ratio).template to_type<int>();
|
||||
}
|
||||
|
||||
ErrorOr<NonnullOwnPtr<WebViewBridge>> WebViewBridge::create(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme preferred_color_scheme, Web::CSS::PreferredContrast preferred_contrast, Web::CSS::PreferredMotion preferred_motion)
|
||||
{
|
||||
return adopt_nonnull_own_or_enomem(new (nothrow) WebViewBridge(move(screen_rects), device_pixel_ratio, preferred_color_scheme, preferred_contrast, preferred_motion));
|
||||
}
|
||||
|
||||
WebViewBridge::WebViewBridge(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme preferred_color_scheme, Web::CSS::PreferredContrast preferred_contrast, Web::CSS::PreferredMotion preferred_motion)
|
||||
: m_screen_rects(move(screen_rects))
|
||||
, m_preferred_color_scheme(preferred_color_scheme)
|
||||
, m_preferred_contrast(preferred_contrast)
|
||||
, m_preferred_motion(preferred_motion)
|
||||
{
|
||||
m_device_pixel_ratio = device_pixel_ratio;
|
||||
}
|
||||
|
||||
WebViewBridge::~WebViewBridge() = default;
|
||||
|
||||
void WebViewBridge::set_device_pixel_ratio(float device_pixel_ratio)
|
||||
{
|
||||
m_device_pixel_ratio = device_pixel_ratio;
|
||||
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
|
||||
}
|
||||
|
||||
void WebViewBridge::set_system_visibility_state(bool is_visible)
|
||||
{
|
||||
client().async_set_system_visibility_state(m_client_state.page_index, is_visible);
|
||||
}
|
||||
|
||||
void WebViewBridge::set_viewport_rect(Gfx::IntRect viewport_rect)
|
||||
{
|
||||
viewport_rect.set_size(scale_for_device(viewport_rect.size(), m_device_pixel_ratio));
|
||||
m_viewport_size = viewport_rect.size();
|
||||
|
||||
handle_resize();
|
||||
}
|
||||
|
||||
void WebViewBridge::update_palette()
|
||||
{
|
||||
auto theme = create_system_palette();
|
||||
client().async_update_system_theme(m_client_state.page_index, move(theme));
|
||||
}
|
||||
|
||||
void WebViewBridge::set_preferred_color_scheme(Web::CSS::PreferredColorScheme color_scheme)
|
||||
{
|
||||
m_preferred_color_scheme = color_scheme;
|
||||
client().async_set_preferred_color_scheme(m_client_state.page_index, color_scheme);
|
||||
}
|
||||
|
||||
void WebViewBridge::set_preferred_contrast(Web::CSS::PreferredContrast contrast)
|
||||
{
|
||||
m_preferred_contrast = contrast;
|
||||
client().async_set_preferred_contrast(m_client_state.page_index, contrast);
|
||||
}
|
||||
|
||||
void WebViewBridge::set_preferred_motion(Web::CSS::PreferredMotion motion)
|
||||
{
|
||||
m_preferred_motion = motion;
|
||||
client().async_set_preferred_motion(m_client_state.page_index, motion);
|
||||
}
|
||||
|
||||
void WebViewBridge::enqueue_input_event(Web::MouseEvent event)
|
||||
{
|
||||
event.position = to_content_position(event.position.to_type<int>()).to_type<Web::DevicePixels>();
|
||||
event.screen_position = to_content_position(event.screen_position.to_type<int>()).to_type<Web::DevicePixels>();
|
||||
ViewImplementation::enqueue_input_event(move(event));
|
||||
}
|
||||
|
||||
void WebViewBridge::enqueue_input_event(Web::DragEvent event)
|
||||
{
|
||||
event.position = to_content_position(event.position.to_type<int>()).to_type<Web::DevicePixels>();
|
||||
event.screen_position = to_content_position(event.screen_position.to_type<int>()).to_type<Web::DevicePixels>();
|
||||
ViewImplementation::enqueue_input_event(move(event));
|
||||
}
|
||||
|
||||
void WebViewBridge::enqueue_input_event(Web::KeyEvent event)
|
||||
{
|
||||
ViewImplementation::enqueue_input_event(move(event));
|
||||
}
|
||||
|
||||
void WebViewBridge::set_enable_autoplay(bool enabled)
|
||||
{
|
||||
ViewImplementation::set_enable_autoplay(enabled);
|
||||
}
|
||||
|
||||
Optional<WebViewBridge::Paintable> WebViewBridge::paintable()
|
||||
{
|
||||
Gfx::Bitmap* bitmap = nullptr;
|
||||
Gfx::IntSize bitmap_size;
|
||||
|
||||
if (m_client_state.has_usable_bitmap) {
|
||||
bitmap = m_client_state.front_bitmap.bitmap.ptr();
|
||||
bitmap_size = m_client_state.front_bitmap.last_painted_size.to_type<int>();
|
||||
} else {
|
||||
bitmap = m_backup_bitmap.ptr();
|
||||
bitmap_size = m_backup_bitmap_size.to_type<int>();
|
||||
}
|
||||
|
||||
if (!bitmap)
|
||||
return {};
|
||||
return Paintable { *bitmap, bitmap_size };
|
||||
}
|
||||
|
||||
void WebViewBridge::update_zoom()
|
||||
{
|
||||
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
|
||||
|
||||
if (on_zoom_level_changed)
|
||||
on_zoom_level_changed();
|
||||
}
|
||||
|
||||
Web::DevicePixelSize WebViewBridge::viewport_size() const
|
||||
{
|
||||
return m_viewport_size.to_type<Web::DevicePixels>();
|
||||
}
|
||||
|
||||
Gfx::IntPoint WebViewBridge::to_content_position(Gfx::IntPoint widget_position) const
|
||||
{
|
||||
return scale_for_device(widget_position, m_device_pixel_ratio);
|
||||
}
|
||||
|
||||
Gfx::IntPoint WebViewBridge::to_widget_position(Gfx::IntPoint content_position) const
|
||||
{
|
||||
return scale_for_device(content_position, inverse_device_pixel_ratio());
|
||||
}
|
||||
|
||||
void WebViewBridge::initialize_client(CreateNewClient create_new_client)
|
||||
{
|
||||
VERIFY(on_request_web_content);
|
||||
|
||||
if (create_new_client == CreateNewClient::Yes) {
|
||||
m_client_state = {};
|
||||
m_client_state.client = on_request_web_content();
|
||||
} else {
|
||||
m_client_state.client->register_view(m_client_state.page_index, *this);
|
||||
}
|
||||
|
||||
m_client_state.client->on_web_content_process_crash = [this] {
|
||||
Core::deferred_invoke([this] {
|
||||
handle_web_content_process_crash();
|
||||
});
|
||||
};
|
||||
|
||||
m_client_state.client_handle = MUST(Web::Crypto::generate_random_uuid());
|
||||
client().async_set_window_handle(m_client_state.page_index, m_client_state.client_handle);
|
||||
|
||||
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio);
|
||||
client().async_set_preferred_color_scheme(m_client_state.page_index, m_preferred_color_scheme);
|
||||
update_palette();
|
||||
|
||||
if (!m_screen_rects.is_empty()) {
|
||||
// FIXME: Update the screens again if they ever change.
|
||||
client().async_update_screen_rects(m_client_state.page_index, m_screen_rects, 0);
|
||||
}
|
||||
|
||||
if (auto const& webdriver_content_ipc_path = WebView::Application::chrome_options().webdriver_content_ipc_path; webdriver_content_ipc_path.has_value()) {
|
||||
client().async_connect_to_webdriver(m_client_state.page_index, *webdriver_content_ipc_path);
|
||||
}
|
||||
|
||||
if (auto const& user_agent_preset = WebView::Application::web_content_options().user_agent_preset; user_agent_preset.has_value()) {
|
||||
auto user_agent = *WebView::user_agents.get(*user_agent_preset);
|
||||
client().async_debug_request(m_client_state.page_index, "spoof-user-agent"sv, user_agent);
|
||||
}
|
||||
}
|
||||
|
||||
void WebViewBridge::initialize_client_as_child(WebViewBridge const& parent, u64 page_index)
|
||||
{
|
||||
m_client_state.client = parent.client();
|
||||
m_client_state.page_index = page_index;
|
||||
|
||||
initialize_client(CreateNewClient::No);
|
||||
}
|
||||
|
||||
}
|
||||
73
UI/AppKit/Interface/LadybirdWebViewBridge.h
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/Point.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/Size.h>
|
||||
#include <LibGfx/StandardCursor.h>
|
||||
#include <LibWeb/CSS/PreferredColorScheme.h>
|
||||
#include <LibWeb/CSS/PreferredContrast.h>
|
||||
#include <LibWeb/CSS/PreferredMotion.h>
|
||||
#include <LibWeb/Page/InputEvent.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
class WebViewBridge final : public WebView::ViewImplementation {
|
||||
public:
|
||||
static ErrorOr<NonnullOwnPtr<WebViewBridge>> create(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme, Web::CSS::PreferredContrast, Web::CSS::PreferredMotion);
|
||||
virtual ~WebViewBridge() override;
|
||||
|
||||
virtual void initialize_client(CreateNewClient = CreateNewClient::Yes) override;
|
||||
void initialize_client_as_child(WebViewBridge const& parent, u64 page_index);
|
||||
|
||||
float device_pixel_ratio() const { return m_device_pixel_ratio; }
|
||||
void set_device_pixel_ratio(float device_pixel_ratio);
|
||||
float inverse_device_pixel_ratio() const { return 1.0f / m_device_pixel_ratio; }
|
||||
|
||||
void set_system_visibility_state(bool is_visible);
|
||||
void set_viewport_rect(Gfx::IntRect);
|
||||
|
||||
void update_palette();
|
||||
void set_preferred_color_scheme(Web::CSS::PreferredColorScheme);
|
||||
void set_preferred_contrast(Web::CSS::PreferredContrast);
|
||||
void set_preferred_motion(Web::CSS::PreferredMotion);
|
||||
|
||||
void enqueue_input_event(Web::MouseEvent);
|
||||
void enqueue_input_event(Web::DragEvent);
|
||||
void enqueue_input_event(Web::KeyEvent);
|
||||
|
||||
void set_enable_autoplay(bool enabled);
|
||||
|
||||
struct Paintable {
|
||||
Gfx::Bitmap& bitmap;
|
||||
Gfx::IntSize bitmap_size;
|
||||
};
|
||||
Optional<Paintable> paintable();
|
||||
|
||||
Function<NonnullRefPtr<WebView::WebContentClient>()> on_request_web_content;
|
||||
Function<void()> on_zoom_level_changed;
|
||||
|
||||
private:
|
||||
WebViewBridge(Vector<Web::DevicePixelRect> screen_rects, float device_pixel_ratio, Web::CSS::PreferredColorScheme, Web::CSS::PreferredContrast, Web::CSS::PreferredMotion);
|
||||
|
||||
virtual void update_zoom() override;
|
||||
virtual Web::DevicePixelSize viewport_size() const override;
|
||||
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override;
|
||||
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override;
|
||||
|
||||
Vector<Web::DevicePixelRect> m_screen_rects;
|
||||
Gfx::IntSize m_viewport_size;
|
||||
|
||||
Web::CSS::PreferredColorScheme m_preferred_color_scheme { Web::CSS::PreferredColorScheme::Auto };
|
||||
Web::CSS::PreferredContrast m_preferred_contrast { Web::CSS::PreferredContrast::Auto };
|
||||
Web::CSS::PreferredMotion m_preferred_motion { Web::CSS::PreferredMotion::Auto };
|
||||
};
|
||||
|
||||
}
|
||||
20
UI/AppKit/Interface/LadybirdWebViewWindow.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class LadybirdWebView;
|
||||
|
||||
@interface LadybirdWebViewWindow : NSWindow
|
||||
|
||||
- (instancetype)initWithWebView:(LadybirdWebView*)web_view
|
||||
windowRect:(NSRect)window_rect;
|
||||
|
||||
@property (nonatomic, strong) LadybirdWebView* web_view;
|
||||
|
||||
@end
|
||||
55
UI/AppKit/Interface/LadybirdWebViewWindow.mm
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/LadybirdWebViewWindow.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
@interface LadybirdWebViewWindow ()
|
||||
@end
|
||||
|
||||
@implementation LadybirdWebViewWindow
|
||||
|
||||
- (instancetype)initWithWebView:(LadybirdWebView*)web_view
|
||||
windowRect:(NSRect)window_rect
|
||||
{
|
||||
static constexpr auto style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
|
||||
|
||||
self = [super initWithContentRect:window_rect
|
||||
styleMask:style_mask
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
if (self) {
|
||||
self.web_view = web_view;
|
||||
|
||||
if (self.web_view == nil)
|
||||
self.web_view = [[LadybirdWebView alloc] init:nil];
|
||||
|
||||
[self.web_view setClipsToBounds:YES];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - NSWindow
|
||||
|
||||
- (void)setIsVisible:(BOOL)flag
|
||||
{
|
||||
[self.web_view handleVisibility:flag];
|
||||
[super setIsVisible:flag];
|
||||
}
|
||||
|
||||
- (void)setIsMiniaturized:(BOOL)flag
|
||||
{
|
||||
[self.web_view handleVisibility:!flag];
|
||||
[super setIsMiniaturized:flag];
|
||||
}
|
||||
|
||||
@end
|
||||
16
UI/AppKit/Interface/Palette.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <LibCore/AnonymousBuffer.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
bool is_using_dark_system_theme();
|
||||
Core::AnonymousBuffer create_system_palette();
|
||||
|
||||
}
|
||||
48
UI/AppKit/Interface/Palette.mm
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <LibCore/Resource.h>
|
||||
#include <LibGfx/Palette.h>
|
||||
#include <LibGfx/SystemTheme.h>
|
||||
#include <UI/Utilities.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Interface/Palette.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
bool is_using_dark_system_theme()
|
||||
{
|
||||
auto* appearance = [NSApp effectiveAppearance];
|
||||
|
||||
auto* matched_appearance = [appearance bestMatchFromAppearancesWithNames:@[
|
||||
NSAppearanceNameAqua,
|
||||
NSAppearanceNameDarkAqua,
|
||||
]];
|
||||
|
||||
return [matched_appearance isEqualToString:NSAppearanceNameDarkAqua];
|
||||
}
|
||||
|
||||
Core::AnonymousBuffer create_system_palette()
|
||||
{
|
||||
auto is_dark = is_using_dark_system_theme();
|
||||
|
||||
auto theme_file = is_dark ? "Dark"sv : "Default"sv;
|
||||
auto theme_ini = MUST(Core::Resource::load_from_uri(MUST(String::formatted("resource://themes/{}.ini", theme_file))));
|
||||
auto theme = Gfx::load_system_theme(theme_ini->filesystem_path().to_byte_string()).release_value_but_fixme_should_propagate_errors();
|
||||
|
||||
auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(theme);
|
||||
auto palette = Gfx::Palette(move(palette_impl));
|
||||
palette.set_flag(Gfx::FlagRole::IsDark, is_dark);
|
||||
palette.set_color(Gfx::ColorRole::Accent, ns_color_to_gfx_color([NSColor controlAccentColor]));
|
||||
// FIXME: There are more system colors we currently don't use (https://developer.apple.com/documentation/appkit/nscolor/3000782-controlaccentcolor?language=objc)
|
||||
|
||||
return theme;
|
||||
}
|
||||
|
||||
}
|
||||
22
UI/AppKit/Interface/SearchPanel.h
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Optional.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface SearchPanel : NSStackView
|
||||
|
||||
- (void)find:(id)selector;
|
||||
- (void)findNextMatch:(id)selector;
|
||||
- (void)findPreviousMatch:(id)selector;
|
||||
- (void)useSelectionForFind:(id)selector;
|
||||
- (void)onFindInPageResult:(size_t)current_match_index
|
||||
totalMatchCount:(Optional<size_t> const&)total_match_count;
|
||||
|
||||
@end
|
||||
225
UI/AppKit/Interface/SearchPanel.mm
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <Interface/LadybirdWebViewBridge.h>
|
||||
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/SearchPanel.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
static constexpr CGFloat const SEARCH_FIELD_HEIGHT = 30;
|
||||
static constexpr CGFloat const SEARCH_FIELD_WIDTH = 300;
|
||||
|
||||
@interface SearchPanel () <NSSearchFieldDelegate>
|
||||
{
|
||||
CaseSensitivity m_case_sensitivity;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSSearchField* search_field;
|
||||
@property (nonatomic, strong) NSButton* search_match_case;
|
||||
@property (nonatomic, strong) NSTextField* result_label;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SearchPanel
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.search_field = [[NSSearchField alloc] init];
|
||||
[self.search_field setPlaceholderString:@"Search"];
|
||||
[self.search_field setDelegate:self];
|
||||
|
||||
auto* search_previous = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameGoLeftTemplate]
|
||||
target:self
|
||||
action:@selector(findPreviousMatch:)];
|
||||
[search_previous setToolTip:@"Find Previous Match"];
|
||||
[search_previous setBordered:NO];
|
||||
|
||||
auto* search_next = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameGoRightTemplate]
|
||||
target:self
|
||||
action:@selector(findNextMatch:)];
|
||||
[search_next setToolTip:@"Find Next Match"];
|
||||
[search_next setBordered:NO];
|
||||
|
||||
self.search_match_case = [NSButton checkboxWithTitle:@"Match Case"
|
||||
target:self
|
||||
action:@selector(find:)];
|
||||
[self.search_match_case setState:NSControlStateValueOff];
|
||||
m_case_sensitivity = CaseSensitivity::CaseInsensitive;
|
||||
|
||||
self.result_label = [NSTextField labelWithString:@""];
|
||||
[self.result_label setHidden:YES];
|
||||
|
||||
auto* search_done = [NSButton buttonWithTitle:@"Done"
|
||||
target:self
|
||||
action:@selector(cancelSearch:)];
|
||||
[search_done setToolTip:@"Close Search Bar"];
|
||||
[search_done setBezelStyle:NSBezelStyleAccessoryBarAction];
|
||||
|
||||
[self addView:self.search_field inGravity:NSStackViewGravityLeading];
|
||||
[self addView:search_previous inGravity:NSStackViewGravityLeading];
|
||||
[self addView:search_next inGravity:NSStackViewGravityLeading];
|
||||
[self addView:self.search_match_case inGravity:NSStackViewGravityLeading];
|
||||
[self addView:self.result_label inGravity:NSStackViewGravityLeading];
|
||||
[self addView:search_done inGravity:NSStackViewGravityTrailing];
|
||||
|
||||
[self setOrientation:NSUserInterfaceLayoutOrientationHorizontal];
|
||||
[self setEdgeInsets:NSEdgeInsets { 0, 8, 0, 8 }];
|
||||
|
||||
[[self heightAnchor] constraintEqualToConstant:SEARCH_FIELD_HEIGHT].active = YES;
|
||||
[[self.search_field widthAnchor] constraintEqualToConstant:SEARCH_FIELD_WIDTH].active = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)find:(id)sender
|
||||
{
|
||||
[self setHidden:NO];
|
||||
[self setSearchTextFromPasteBoard];
|
||||
|
||||
[self.window makeFirstResponder:self.search_field];
|
||||
}
|
||||
|
||||
- (void)findNextMatch:(id)sender
|
||||
{
|
||||
if ([self setSearchTextFromPasteBoard]) {
|
||||
return;
|
||||
}
|
||||
|
||||
[[[self tab] web_view] findInPageNextMatch];
|
||||
}
|
||||
|
||||
- (void)findPreviousMatch:(id)sender
|
||||
{
|
||||
if ([self setSearchTextFromPasteBoard]) {
|
||||
return;
|
||||
}
|
||||
|
||||
[[[self tab] web_view] findInPagePreviousMatch];
|
||||
}
|
||||
|
||||
- (void)useSelectionForFind:(id)sender
|
||||
{
|
||||
auto selected_text = [[[self tab] web_view] view].selected_text();
|
||||
auto* query = Ladybird::string_to_ns_string(selected_text);
|
||||
|
||||
[self setPasteBoardContents:query];
|
||||
|
||||
if (![self isHidden]) {
|
||||
[self.search_field setStringValue:query];
|
||||
[[[self tab] web_view] findInPage:query caseSensitivity:m_case_sensitivity];
|
||||
|
||||
[self.window makeFirstResponder:self.search_field];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onFindInPageResult:(size_t)current_match_index
|
||||
totalMatchCount:(Optional<size_t> const&)total_match_count
|
||||
{
|
||||
if (total_match_count.has_value()) {
|
||||
auto* label_text = *total_match_count > 0
|
||||
? [NSString stringWithFormat:@"%zu of %zu matches", current_match_index, *total_match_count]
|
||||
: @"Phrase not found";
|
||||
|
||||
auto* label_attributes = @{
|
||||
NSFontAttributeName : [NSFont boldSystemFontOfSize:12.0f],
|
||||
};
|
||||
|
||||
auto* label_attribute = [[NSAttributedString alloc] initWithString:label_text
|
||||
attributes:label_attributes];
|
||||
|
||||
[self.result_label setAttributedStringValue:label_attribute];
|
||||
[self.result_label setHidden:NO];
|
||||
} else {
|
||||
[self.result_label setHidden:YES];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (Tab*)tab
|
||||
{
|
||||
return (Tab*)[self window];
|
||||
}
|
||||
|
||||
- (void)setPasteBoardContents:(NSString*)query
|
||||
{
|
||||
auto* paste_board = [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
|
||||
[paste_board clearContents];
|
||||
[paste_board setString:query forType:NSPasteboardTypeString];
|
||||
}
|
||||
|
||||
- (BOOL)setSearchTextFromPasteBoard
|
||||
{
|
||||
auto* paste_board = [NSPasteboard pasteboardWithName:NSPasteboardNameFind];
|
||||
auto* query = [paste_board stringForType:NSPasteboardTypeString];
|
||||
|
||||
if (query) {
|
||||
auto case_sensitivity = [self.search_match_case state] == NSControlStateValueOff
|
||||
? CaseSensitivity::CaseInsensitive
|
||||
: CaseSensitivity::CaseSensitive;
|
||||
|
||||
if (case_sensitivity != m_case_sensitivity || ![[self.search_field stringValue] isEqual:query]) {
|
||||
[self.search_field setStringValue:query];
|
||||
m_case_sensitivity = case_sensitivity;
|
||||
|
||||
[[[self tab] web_view] findInPage:query caseSensitivity:m_case_sensitivity];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)cancelSearch:(id)sender
|
||||
{
|
||||
[self setHidden:YES];
|
||||
}
|
||||
|
||||
#pragma mark - NSSearchFieldDelegate
|
||||
|
||||
- (void)controlTextDidChange:(NSNotification*)notification
|
||||
{
|
||||
auto* query = [self.search_field stringValue];
|
||||
[[[self tab] web_view] findInPage:query caseSensitivity:m_case_sensitivity];
|
||||
|
||||
[self setPasteBoardContents:query];
|
||||
}
|
||||
|
||||
- (BOOL)control:(NSControl*)control
|
||||
textView:(NSTextView*)text_view
|
||||
doCommandBySelector:(SEL)selector
|
||||
{
|
||||
if (selector == @selector(insertNewline:)) {
|
||||
NSEvent* event = [[self tab] currentEvent];
|
||||
|
||||
if ((event.modifierFlags & NSEventModifierFlagShift) == 0) {
|
||||
[self findNextMatch:nil];
|
||||
} else {
|
||||
[self findPreviousMatch:nil];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (selector == @selector(cancelOperation:)) {
|
||||
[self cancelSearch:nil];
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
27
UI/AppKit/Interface/Tab.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Types.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Interface/LadybirdWebViewWindow.h>
|
||||
|
||||
@class LadybirdWebView;
|
||||
|
||||
@interface Tab : LadybirdWebViewWindow
|
||||
|
||||
- (instancetype)init;
|
||||
- (instancetype)initAsChild:(Tab*)parent
|
||||
pageIndex:(u64)page_index;
|
||||
|
||||
- (void)tabWillClose;
|
||||
|
||||
- (void)openInspector:(id)sender;
|
||||
- (void)onInspectorClosed;
|
||||
|
||||
@end
|
||||
396
UI/AppKit/Interface/Tab.mm
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/String.h>
|
||||
#include <LibCore/Resource.h>
|
||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
||||
#include <LibGfx/ShareableBitmap.h>
|
||||
#include <LibURL/URL.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <UI/Utilities.h>
|
||||
|
||||
#import <Application/ApplicationDelegate.h>
|
||||
#import <Interface/Inspector.h>
|
||||
#import <Interface/InspectorController.h>
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/SearchPanel.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Interface/TabController.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
static constexpr CGFloat const WINDOW_WIDTH = 1000;
|
||||
static constexpr CGFloat const WINDOW_HEIGHT = 800;
|
||||
|
||||
@interface Tab () <LadybirdWebViewObserver>
|
||||
|
||||
@property (nonatomic, strong) NSString* title;
|
||||
@property (nonatomic, strong) NSImage* favicon;
|
||||
|
||||
@property (nonatomic, strong) SearchPanel* search_panel;
|
||||
|
||||
@property (nonatomic, strong) InspectorController* inspector_controller;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Tab
|
||||
|
||||
@dynamic title;
|
||||
|
||||
+ (NSImage*)defaultFavicon
|
||||
{
|
||||
static NSImage* default_favicon;
|
||||
static dispatch_once_t token;
|
||||
|
||||
dispatch_once(&token, ^{
|
||||
auto default_favicon_path = MUST(Core::Resource::load_from_uri("resource://icons/48x48/app-browser.png"sv));
|
||||
auto* ns_default_favicon_path = Ladybird::string_to_ns_string(default_favicon_path->filesystem_path());
|
||||
|
||||
default_favicon = [[NSImage alloc] initWithContentsOfFile:ns_default_favicon_path];
|
||||
});
|
||||
|
||||
return default_favicon;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
auto* web_view = [[LadybirdWebView alloc] init:self];
|
||||
return [self initWithWebView:web_view];
|
||||
}
|
||||
|
||||
- (instancetype)initAsChild:(Tab*)parent
|
||||
pageIndex:(u64)page_index
|
||||
{
|
||||
auto* web_view = [[LadybirdWebView alloc] initAsChild:self parent:[parent web_view] pageIndex:page_index];
|
||||
return [self initWithWebView:web_view];
|
||||
}
|
||||
|
||||
- (instancetype)initWithWebView:(LadybirdWebView*)web_view
|
||||
{
|
||||
auto screen_rect = [[NSScreen mainScreen] frame];
|
||||
auto position_x = (NSWidth(screen_rect) - WINDOW_WIDTH) / 2;
|
||||
auto position_y = (NSHeight(screen_rect) - WINDOW_HEIGHT) / 2;
|
||||
auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
|
||||
if (self = [super initWithWebView:web_view windowRect:window_rect]) {
|
||||
// Remember last window position
|
||||
self.frameAutosaveName = @"window";
|
||||
|
||||
self.favicon = [Tab defaultFavicon];
|
||||
self.title = @"New Tab";
|
||||
[self updateTabTitleAndFavicon];
|
||||
|
||||
[self setTitleVisibility:NSWindowTitleHidden];
|
||||
[self setIsVisible:YES];
|
||||
|
||||
self.search_panel = [[SearchPanel alloc] init];
|
||||
[self.search_panel setHidden:YES];
|
||||
|
||||
auto* stack_view = [NSStackView stackViewWithViews:@[
|
||||
self.search_panel,
|
||||
self.web_view,
|
||||
]];
|
||||
|
||||
[stack_view setOrientation:NSUserInterfaceLayoutOrientationVertical];
|
||||
[stack_view setSpacing:0];
|
||||
|
||||
[self setContentView:stack_view];
|
||||
|
||||
[[self.search_panel leadingAnchor] constraintEqualToAnchor:[self.contentView leadingAnchor]].active = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)find:(id)sender
|
||||
{
|
||||
[self.search_panel find:sender];
|
||||
}
|
||||
|
||||
- (void)findNextMatch:(id)sender
|
||||
{
|
||||
[self.search_panel findNextMatch:sender];
|
||||
}
|
||||
|
||||
- (void)findPreviousMatch:(id)sender
|
||||
{
|
||||
[self.search_panel findPreviousMatch:sender];
|
||||
}
|
||||
|
||||
- (void)useSelectionForFind:(id)sender
|
||||
{
|
||||
[self.search_panel useSelectionForFind:sender];
|
||||
}
|
||||
|
||||
- (void)tabWillClose
|
||||
{
|
||||
if (self.inspector_controller != nil) {
|
||||
[self.inspector_controller.window close];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openInspector:(id)sender
|
||||
{
|
||||
if (self.inspector_controller != nil) {
|
||||
[self.inspector_controller.window makeKeyAndOrderFront:sender];
|
||||
return;
|
||||
}
|
||||
|
||||
self.inspector_controller = [[InspectorController alloc] init:self];
|
||||
[self.inspector_controller showWindow:nil];
|
||||
}
|
||||
|
||||
- (void)onInspectorClosed
|
||||
{
|
||||
self.inspector_controller = nil;
|
||||
}
|
||||
|
||||
- (void)inspectElement:(id)sender
|
||||
{
|
||||
[self openInspector:sender];
|
||||
|
||||
auto* inspector = (Inspector*)[self.inspector_controller window];
|
||||
[inspector selectHoveredElement];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (TabController*)tabController
|
||||
{
|
||||
return (TabController*)[self windowController];
|
||||
}
|
||||
|
||||
- (void)updateTabTitleAndFavicon
|
||||
{
|
||||
static constexpr CGFloat TITLE_FONT_SIZE = 12;
|
||||
static constexpr CGFloat FAVICON_SIZE = 16;
|
||||
|
||||
NSFont* title_font = [NSFont systemFontOfSize:TITLE_FONT_SIZE];
|
||||
|
||||
auto* favicon_attachment = [[NSTextAttachment alloc] init];
|
||||
favicon_attachment.image = self.favicon;
|
||||
|
||||
// By default, the image attachment will "automatically adapt to the surrounding font and color
|
||||
// attributes in attributed strings". Therefore, we specify a clear color here to prevent the
|
||||
// favicon from having a weird tint.
|
||||
auto* favicon_attribute = (NSMutableAttributedString*)[NSMutableAttributedString attributedStringWithAttachment:favicon_attachment];
|
||||
[favicon_attribute addAttribute:NSForegroundColorAttributeName
|
||||
value:[NSColor clearColor]
|
||||
range:NSMakeRange(0, [favicon_attribute length])];
|
||||
|
||||
// adjust the favicon image to middle center the title text
|
||||
CGFloat offset_y = (title_font.capHeight - FAVICON_SIZE) / 2.f;
|
||||
[favicon_attachment setBounds:CGRectMake(0, offset_y, FAVICON_SIZE, FAVICON_SIZE)];
|
||||
|
||||
auto* title_attributes = @{
|
||||
NSForegroundColorAttributeName : [NSColor textColor],
|
||||
NSFontAttributeName : title_font
|
||||
};
|
||||
|
||||
auto* title_attribute = [[NSAttributedString alloc] initWithString:self.title
|
||||
attributes:title_attributes];
|
||||
|
||||
auto* spacing_attribute = [[NSAttributedString alloc] initWithString:@" "
|
||||
attributes:title_attributes];
|
||||
|
||||
auto* title_and_favicon = [[NSMutableAttributedString alloc] init];
|
||||
[title_and_favicon appendAttributedString:favicon_attribute];
|
||||
[title_and_favicon appendAttributedString:spacing_attribute];
|
||||
[title_and_favicon appendAttributedString:title_attribute];
|
||||
|
||||
[[self tab] setAttributedTitle:title_and_favicon];
|
||||
}
|
||||
|
||||
- (void)togglePageMuteState:(id)button
|
||||
{
|
||||
auto& view = [[self web_view] view];
|
||||
view.toggle_page_mute_state();
|
||||
|
||||
switch (view.audio_play_state()) {
|
||||
case Web::HTML::AudioPlayState::Paused:
|
||||
[[self tab] setAccessoryView:nil];
|
||||
break;
|
||||
|
||||
case Web::HTML::AudioPlayState::Playing:
|
||||
[button setImage:[self iconForPageMuteState]];
|
||||
[button setToolTip:[self toolTipForPageMuteState]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSImage*)iconForPageMuteState
|
||||
{
|
||||
auto& view = [[self web_view] view];
|
||||
|
||||
switch (view.page_mute_state()) {
|
||||
case Web::HTML::MuteState::Muted:
|
||||
return [NSImage imageNamed:NSImageNameTouchBarAudioOutputVolumeOffTemplate];
|
||||
case Web::HTML::MuteState::Unmuted:
|
||||
return [NSImage imageNamed:NSImageNameTouchBarAudioOutputVolumeHighTemplate];
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
- (NSString*)toolTipForPageMuteState
|
||||
{
|
||||
auto& view = [[self web_view] view];
|
||||
|
||||
switch (view.page_mute_state()) {
|
||||
case Web::HTML::MuteState::Muted:
|
||||
return @"Unmute tab";
|
||||
case Web::HTML::MuteState::Unmuted:
|
||||
return @"Mute tab";
|
||||
}
|
||||
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
#pragma mark - LadybirdWebViewObserver
|
||||
|
||||
- (String const&)onCreateNewTab:(Optional<URL::URL> const&)url
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
{
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
|
||||
auto* controller = [delegate createNewTab:url
|
||||
fromTab:self
|
||||
activateTab:activate_tab];
|
||||
|
||||
auto* tab = (Tab*)[controller window];
|
||||
return [[tab web_view] handle];
|
||||
}
|
||||
|
||||
- (String const&)onCreateNewTab:(StringView)html
|
||||
url:(URL::URL const&)url
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
{
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
|
||||
auto* controller = [delegate createNewTab:html
|
||||
url:url
|
||||
fromTab:self
|
||||
activateTab:activate_tab];
|
||||
|
||||
auto* tab = (Tab*)[controller window];
|
||||
return [[tab web_view] handle];
|
||||
}
|
||||
|
||||
- (String const&)onCreateChildTab:(Optional<URL::URL> const&)url
|
||||
activateTab:(Web::HTML::ActivateTab)activate_tab
|
||||
pageIndex:(u64)page_index
|
||||
{
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
|
||||
auto* controller = [delegate createChildTab:url
|
||||
fromTab:self
|
||||
activateTab:activate_tab
|
||||
pageIndex:page_index];
|
||||
|
||||
auto* tab = (Tab*)[controller window];
|
||||
return [[tab web_view] handle];
|
||||
}
|
||||
|
||||
- (void)loadURL:(URL::URL const&)url
|
||||
{
|
||||
[[self tabController] loadURL:url];
|
||||
}
|
||||
|
||||
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)is_redirect
|
||||
{
|
||||
self.title = Ladybird::string_to_ns_string(url.serialize());
|
||||
self.favicon = [Tab defaultFavicon];
|
||||
[self updateTabTitleAndFavicon];
|
||||
|
||||
[[self tabController] onLoadStart:url isRedirect:is_redirect];
|
||||
|
||||
if (self.inspector_controller != nil) {
|
||||
auto* inspector = (Inspector*)[self.inspector_controller window];
|
||||
[inspector reset];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onLoadFinish:(URL::URL const&)url
|
||||
{
|
||||
if (self.inspector_controller != nil) {
|
||||
auto* inspector = (Inspector*)[self.inspector_controller window];
|
||||
[inspector inspect];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url
|
||||
{
|
||||
[[self tabController] onURLChange:url];
|
||||
}
|
||||
|
||||
- (void)onBackNavigationEnabled:(BOOL)back_enabled
|
||||
forwardNavigationEnabled:(BOOL)forward_enabled
|
||||
{
|
||||
[[self tabController] onBackNavigationEnabled:back_enabled
|
||||
forwardNavigationEnabled:forward_enabled];
|
||||
}
|
||||
|
||||
- (void)onTitleChange:(ByteString const&)title
|
||||
{
|
||||
[[self tabController] onTitleChange:title];
|
||||
|
||||
self.title = Ladybird::string_to_ns_string(title);
|
||||
[self updateTabTitleAndFavicon];
|
||||
}
|
||||
|
||||
- (void)onFaviconChange:(Gfx::Bitmap const&)bitmap
|
||||
{
|
||||
auto png = Gfx::PNGWriter::encode(bitmap);
|
||||
if (png.is_error()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* data = [NSData dataWithBytes:png.value().data()
|
||||
length:png.value().size()];
|
||||
|
||||
auto* favicon = [[NSImage alloc] initWithData:data];
|
||||
[favicon setResizingMode:NSImageResizingModeStretch];
|
||||
|
||||
self.favicon = favicon;
|
||||
[self updateTabTitleAndFavicon];
|
||||
}
|
||||
|
||||
- (void)onAudioPlayStateChange:(Web::HTML::AudioPlayState)play_state
|
||||
{
|
||||
auto& view = [[self web_view] view];
|
||||
|
||||
switch (play_state) {
|
||||
case Web::HTML::AudioPlayState::Paused:
|
||||
if (view.page_mute_state() == Web::HTML::MuteState::Unmuted) {
|
||||
[[self tab] setAccessoryView:nil];
|
||||
}
|
||||
break;
|
||||
|
||||
case Web::HTML::AudioPlayState::Playing:
|
||||
auto* button = [NSButton buttonWithImage:[self iconForPageMuteState]
|
||||
target:self
|
||||
action:@selector(togglePageMuteState:)];
|
||||
[button setToolTip:[self toolTipForPageMuteState]];
|
||||
|
||||
[[self tab] setAccessoryView:button];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onFindInPageResult:(size_t)current_match_index
|
||||
totalMatchCount:(Optional<size_t> const&)total_match_count
|
||||
{
|
||||
[self.search_panel onFindInPageResult:current_match_index
|
||||
totalMatchCount:total_match_count];
|
||||
}
|
||||
|
||||
@end
|
||||
57
UI/AppKit/Interface/TabController.h
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <LibURL/URL.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class Tab;
|
||||
|
||||
struct TabSettings {
|
||||
BOOL should_show_line_box_borders { NO };
|
||||
BOOL scripting_enabled { YES };
|
||||
BOOL block_popups { YES };
|
||||
BOOL autoplay_enabled { NO };
|
||||
BOOL same_origin_policy_enabled { NO };
|
||||
ByteString user_agent_name { "Disabled"sv };
|
||||
ByteString navigator_compatibility_mode { "chrome"sv };
|
||||
};
|
||||
|
||||
@interface TabController : NSWindowController <NSWindowDelegate>
|
||||
|
||||
- (instancetype)init;
|
||||
- (instancetype)initAsChild:(Tab*)parent
|
||||
pageIndex:(u64)page_index;
|
||||
|
||||
- (void)loadURL:(URL::URL const&)url;
|
||||
- (void)loadHTML:(StringView)html url:(URL::URL const&)url;
|
||||
|
||||
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect;
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url;
|
||||
- (void)onBackNavigationEnabled:(BOOL)back_enabled
|
||||
forwardNavigationEnabled:(BOOL)forward_enabled;
|
||||
|
||||
- (void)onTitleChange:(ByteString const&)title;
|
||||
|
||||
- (void)onCreateNewTab;
|
||||
|
||||
- (void)navigateBack:(id)sender;
|
||||
- (void)navigateForward:(id)sender;
|
||||
- (void)reload:(id)sender;
|
||||
- (void)clearHistory;
|
||||
|
||||
- (void)setPopupBlocking:(BOOL)block_popups;
|
||||
- (void)setScripting:(BOOL)enabled;
|
||||
- (void)setAutoplay:(BOOL)enabled;
|
||||
- (void)debugRequest:(ByteString const&)request argument:(ByteString const&)argument;
|
||||
|
||||
- (void)focusLocationToolbarItem;
|
||||
|
||||
@end
|
||||
725
UI/AppKit/Interface/TabController.mm
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <LibWeb/Loader/UserAgent.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/SearchEngine.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <LibWebView/UserAgent.h>
|
||||
|
||||
#import <Application/ApplicationDelegate.h>
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Interface/TabController.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
static NSString* const TOOLBAR_IDENTIFIER = @"Toolbar";
|
||||
static NSString* const TOOLBAR_NAVIGATE_BACK_IDENTIFIER = @"ToolbarNavigateBackIdentifier";
|
||||
static NSString* const TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER = @"ToolbarNavigateForwardIdentifier";
|
||||
static NSString* const TOOLBAR_RELOAD_IDENTIFIER = @"ToolbarReloadIdentifier";
|
||||
static NSString* const TOOLBAR_LOCATION_IDENTIFIER = @"ToolbarLocationIdentifier";
|
||||
static NSString* const TOOLBAR_ZOOM_IDENTIFIER = @"ToolbarZoomIdentifier";
|
||||
static NSString* const TOOLBAR_NEW_TAB_IDENTIFIER = @"ToolbarNewTabIdentifier";
|
||||
static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIdentifer";
|
||||
|
||||
@interface LocationSearchField : NSSearchField
|
||||
|
||||
- (BOOL)becomeFirstResponder;
|
||||
|
||||
@end
|
||||
|
||||
@implementation LocationSearchField
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
BOOL result = [super becomeFirstResponder];
|
||||
if (result)
|
||||
[self performSelector:@selector(selectText:) withObject:self afterDelay:0];
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface TabController () <NSToolbarDelegate, NSSearchFieldDelegate>
|
||||
{
|
||||
u64 m_page_index;
|
||||
|
||||
ByteString m_title;
|
||||
|
||||
TabSettings m_settings;
|
||||
|
||||
bool m_can_navigate_back;
|
||||
bool m_can_navigate_forward;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) Tab* parent;
|
||||
|
||||
@property (nonatomic, strong) NSToolbar* toolbar;
|
||||
@property (nonatomic, strong) NSArray* toolbar_identifiers;
|
||||
|
||||
@property (nonatomic, strong) NSToolbarItem* navigate_back_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* navigate_forward_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* reload_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* location_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* zoom_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* new_tab_toolbar_item;
|
||||
@property (nonatomic, strong) NSToolbarItem* tab_overview_toolbar_item;
|
||||
|
||||
@property (nonatomic, assign) NSLayoutConstraint* location_toolbar_item_width;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TabController
|
||||
|
||||
@synthesize toolbar_identifiers = _toolbar_identifiers;
|
||||
@synthesize navigate_back_toolbar_item = _navigate_back_toolbar_item;
|
||||
@synthesize navigate_forward_toolbar_item = _navigate_forward_toolbar_item;
|
||||
@synthesize reload_toolbar_item = _reload_toolbar_item;
|
||||
@synthesize location_toolbar_item = _location_toolbar_item;
|
||||
@synthesize zoom_toolbar_item = _zoom_toolbar_item;
|
||||
@synthesize new_tab_toolbar_item = _new_tab_toolbar_item;
|
||||
@synthesize tab_overview_toolbar_item = _tab_overview_toolbar_item;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.toolbar = [[NSToolbar alloc] initWithIdentifier:TOOLBAR_IDENTIFIER];
|
||||
[self.toolbar setDelegate:self];
|
||||
[self.toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
|
||||
[self.toolbar setAllowsUserCustomization:NO];
|
||||
[self.toolbar setSizeMode:NSToolbarSizeModeRegular];
|
||||
|
||||
m_page_index = 0;
|
||||
|
||||
m_settings = {
|
||||
.scripting_enabled = WebView::Application::chrome_options().disable_scripting == WebView::DisableScripting::Yes ? NO : YES,
|
||||
.block_popups = WebView::Application::chrome_options().allow_popups == WebView::AllowPopups::Yes ? NO : YES,
|
||||
.autoplay_enabled = WebView::Application::web_content_options().enable_autoplay == WebView::EnableAutoplay::Yes ? YES : NO,
|
||||
};
|
||||
|
||||
if (auto const& user_agent_preset = WebView::Application::web_content_options().user_agent_preset; user_agent_preset.has_value())
|
||||
m_settings.user_agent_name = *user_agent_preset;
|
||||
|
||||
m_can_navigate_back = false;
|
||||
m_can_navigate_forward = false;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initAsChild:(Tab*)parent
|
||||
pageIndex:(u64)page_index
|
||||
{
|
||||
if (self = [self init]) {
|
||||
self.parent = parent;
|
||||
m_page_index = page_index;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)loadURL:(URL::URL const&)url
|
||||
{
|
||||
[[self tab].web_view loadURL:url];
|
||||
}
|
||||
|
||||
- (void)loadHTML:(StringView)html url:(URL::URL const&)url
|
||||
{
|
||||
[[self tab].web_view loadHTML:html];
|
||||
}
|
||||
|
||||
- (void)onLoadStart:(URL::URL const&)url isRedirect:(BOOL)isRedirect
|
||||
{
|
||||
[self setLocationFieldText:url.serialize()];
|
||||
}
|
||||
|
||||
- (void)onURLChange:(URL::URL const&)url
|
||||
{
|
||||
[self setLocationFieldText:url.serialize()];
|
||||
}
|
||||
|
||||
- (void)onBackNavigationEnabled:(BOOL)back_enabled
|
||||
forwardNavigationEnabled:(BOOL)forward_enabled
|
||||
{
|
||||
m_can_navigate_back = back_enabled;
|
||||
m_can_navigate_forward = forward_enabled;
|
||||
[self updateNavigationButtonStates];
|
||||
}
|
||||
|
||||
- (void)onTitleChange:(ByteString const&)title
|
||||
{
|
||||
m_title = title;
|
||||
}
|
||||
|
||||
- (void)onCreateNewTab
|
||||
{
|
||||
[self setPopupBlocking:m_settings.block_popups];
|
||||
[self setScripting:m_settings.scripting_enabled];
|
||||
[self setAutoplay:m_settings.autoplay_enabled];
|
||||
}
|
||||
|
||||
- (void)zoomIn:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] zoomIn];
|
||||
[self updateZoomButton];
|
||||
}
|
||||
|
||||
- (void)zoomOut:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] zoomOut];
|
||||
[self updateZoomButton];
|
||||
}
|
||||
|
||||
- (void)resetZoom:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] resetZoom];
|
||||
[self updateZoomButton];
|
||||
}
|
||||
|
||||
- (void)navigateBack:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] navigateBack];
|
||||
}
|
||||
|
||||
- (void)navigateForward:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] navigateForward];
|
||||
}
|
||||
|
||||
- (void)reload:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] reload];
|
||||
}
|
||||
|
||||
- (void)clearHistory
|
||||
{
|
||||
// FIXME: Reimplement clearing history using WebContent's history.
|
||||
}
|
||||
|
||||
- (void)debugRequest:(ByteString const&)request argument:(ByteString const&)argument
|
||||
{
|
||||
[[[self tab] web_view] debugRequest:request argument:argument];
|
||||
}
|
||||
|
||||
- (void)viewSource:(id)sender
|
||||
{
|
||||
[[[self tab] web_view] viewSource];
|
||||
}
|
||||
|
||||
- (void)focusLocationToolbarItem
|
||||
{
|
||||
[self.window makeFirstResponder:self.location_toolbar_item.view];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (Tab*)tab
|
||||
{
|
||||
return (Tab*)[self window];
|
||||
}
|
||||
|
||||
- (void)createNewTab:(id)sender
|
||||
{
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
|
||||
self.tab.titlebarAppearsTransparent = NO;
|
||||
|
||||
[delegate createNewTab:WebView::Application::chrome_options().new_tab_page_url
|
||||
fromTab:[self tab]
|
||||
activateTab:Web::HTML::ActivateTab::Yes];
|
||||
|
||||
self.tab.titlebarAppearsTransparent = YES;
|
||||
}
|
||||
|
||||
- (void)setLocationFieldText:(StringView)url
|
||||
{
|
||||
NSMutableAttributedString* attributed_url;
|
||||
|
||||
auto* dark_attributes = @{
|
||||
NSForegroundColorAttributeName : [NSColor systemGrayColor],
|
||||
};
|
||||
auto* highlight_attributes = @{
|
||||
NSForegroundColorAttributeName : [NSColor textColor],
|
||||
};
|
||||
|
||||
if (auto url_parts = WebView::break_url_into_parts(url); url_parts.has_value()) {
|
||||
attributed_url = [[NSMutableAttributedString alloc] init];
|
||||
|
||||
auto* attributed_scheme_and_subdomain = [[NSAttributedString alloc]
|
||||
initWithString:Ladybird::string_to_ns_string(url_parts->scheme_and_subdomain)
|
||||
attributes:dark_attributes];
|
||||
|
||||
auto* attributed_effective_tld_plus_one = [[NSAttributedString alloc]
|
||||
initWithString:Ladybird::string_to_ns_string(url_parts->effective_tld_plus_one)
|
||||
attributes:highlight_attributes];
|
||||
|
||||
auto* attributed_remainder = [[NSAttributedString alloc]
|
||||
initWithString:Ladybird::string_to_ns_string(url_parts->remainder)
|
||||
attributes:dark_attributes];
|
||||
|
||||
[attributed_url appendAttributedString:attributed_scheme_and_subdomain];
|
||||
[attributed_url appendAttributedString:attributed_effective_tld_plus_one];
|
||||
[attributed_url appendAttributedString:attributed_remainder];
|
||||
} else {
|
||||
attributed_url = [[NSMutableAttributedString alloc]
|
||||
initWithString:Ladybird::string_to_ns_string(url)
|
||||
attributes:highlight_attributes];
|
||||
}
|
||||
|
||||
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
|
||||
[location_search_field setAttributedStringValue:attributed_url];
|
||||
}
|
||||
|
||||
- (void)updateNavigationButtonStates
|
||||
{
|
||||
auto* navigate_back_button = (NSButton*)[[self navigate_back_toolbar_item] view];
|
||||
[navigate_back_button setEnabled:m_can_navigate_back];
|
||||
|
||||
auto* navigate_forward_button = (NSButton*)[[self navigate_forward_toolbar_item] view];
|
||||
[navigate_forward_button setEnabled:m_can_navigate_forward];
|
||||
}
|
||||
|
||||
- (void)showTabOverview:(id)sender
|
||||
{
|
||||
self.tab.titlebarAppearsTransparent = NO;
|
||||
[self.window toggleTabOverview:sender];
|
||||
self.tab.titlebarAppearsTransparent = YES;
|
||||
}
|
||||
|
||||
- (void)updateZoomButton
|
||||
{
|
||||
auto zoom_level = [[[self tab] web_view] zoomLevel];
|
||||
|
||||
auto* zoom_level_text = [NSString stringWithFormat:@"%d%%", round_to<int>(zoom_level * 100.0f)];
|
||||
[self.zoom_toolbar_item setTitle:zoom_level_text];
|
||||
|
||||
auto zoom_button_hidden = zoom_level == 1.0 ? YES : NO;
|
||||
[[self.zoom_toolbar_item view] setHidden:zoom_button_hidden];
|
||||
}
|
||||
|
||||
- (void)dumpDOMTree:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-dom-tree" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpLayoutTree:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-layout-tree" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpPaintTree:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-paint-tree" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpStackingContextTree:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-stacking-context-tree" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpStyleSheets:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-style-sheets" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpAllResolvedStyles:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-all-resolved-styles" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpHistory:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-session-history" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpLocalStorage:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-local-storage" argument:""];
|
||||
}
|
||||
|
||||
- (void)toggleLineBoxBorders:(id)sender
|
||||
{
|
||||
m_settings.should_show_line_box_borders = !m_settings.should_show_line_box_borders;
|
||||
[self debugRequest:"set-line-box-borders" argument:m_settings.should_show_line_box_borders ? "on" : "off"];
|
||||
}
|
||||
|
||||
- (void)collectGarbage:(id)sender
|
||||
{
|
||||
[self debugRequest:"collect-garbage" argument:""];
|
||||
}
|
||||
|
||||
- (void)dumpGCGraph:(id)sender
|
||||
{
|
||||
[self debugRequest:"dump-gc-graph" argument:""];
|
||||
}
|
||||
|
||||
- (void)clearCache:(id)sender
|
||||
{
|
||||
[self debugRequest:"clear-cache" argument:""];
|
||||
}
|
||||
|
||||
- (void)toggleScripting:(id)sender
|
||||
{
|
||||
m_settings.scripting_enabled = !m_settings.scripting_enabled;
|
||||
[self setScripting:m_settings.scripting_enabled];
|
||||
}
|
||||
|
||||
- (void)setScripting:(BOOL)enabled
|
||||
{
|
||||
[self debugRequest:"scripting" argument:enabled ? "on" : "off"];
|
||||
}
|
||||
|
||||
- (void)togglePopupBlocking:(id)sender
|
||||
{
|
||||
m_settings.block_popups = !m_settings.block_popups;
|
||||
[self setPopupBlocking:m_settings.block_popups];
|
||||
}
|
||||
|
||||
- (void)setPopupBlocking:(BOOL)block_popups
|
||||
{
|
||||
[self debugRequest:"block-pop-ups" argument:block_popups ? "on" : "off"];
|
||||
}
|
||||
|
||||
- (void)toggleAutoplay:(id)sender
|
||||
{
|
||||
m_settings.autoplay_enabled = !m_settings.autoplay_enabled;
|
||||
[self setAutoplay:m_settings.autoplay_enabled];
|
||||
}
|
||||
|
||||
- (void)setAutoplay:(BOOL)enabled
|
||||
{
|
||||
[[[self tab] web_view] setEnableAutoplay:m_settings.autoplay_enabled];
|
||||
}
|
||||
|
||||
- (void)toggleSameOriginPolicy:(id)sender
|
||||
{
|
||||
m_settings.same_origin_policy_enabled = !m_settings.same_origin_policy_enabled;
|
||||
[self debugRequest:"same-origin-policy" argument:m_settings.same_origin_policy_enabled ? "on" : "off"];
|
||||
}
|
||||
|
||||
- (void)setUserAgentSpoof:(NSMenuItem*)sender
|
||||
{
|
||||
ByteString const user_agent_name = [[sender title] UTF8String];
|
||||
ByteString user_agent = "";
|
||||
if (user_agent_name == "Disabled"sv) {
|
||||
user_agent = Web::default_user_agent;
|
||||
} else {
|
||||
user_agent = WebView::user_agents.get(user_agent_name).value();
|
||||
}
|
||||
m_settings.user_agent_name = user_agent_name;
|
||||
|
||||
[self debugRequest:"spoof-user-agent" argument:user_agent];
|
||||
[self debugRequest:"clear-cache" argument:""]; // clear the cache to ensure requests are re-done with the new user agent
|
||||
}
|
||||
|
||||
- (void)setNavigatorCompatibilityMode:(NSMenuItem*)sender
|
||||
{
|
||||
ByteString const compatibility_mode = [[[sender title] lowercaseString] UTF8String];
|
||||
m_settings.navigator_compatibility_mode = compatibility_mode;
|
||||
|
||||
[self debugRequest:"navigator-compatibility-mode" argument:compatibility_mode];
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (NSButton*)create_button:(NSImageName)image
|
||||
with_action:(nonnull SEL)action
|
||||
with_tooltip:(NSString*)tooltip
|
||||
{
|
||||
auto* button = [NSButton buttonWithImage:[NSImage imageNamed:image]
|
||||
target:self
|
||||
action:action];
|
||||
if (tooltip) {
|
||||
[button setToolTip:tooltip];
|
||||
}
|
||||
|
||||
[button setBordered:YES];
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)navigate_back_toolbar_item
|
||||
{
|
||||
if (!_navigate_back_toolbar_item) {
|
||||
auto* button = [self create_button:NSImageNameGoBackTemplate
|
||||
with_action:@selector(navigateBack:)
|
||||
with_tooltip:@"Navigate back"];
|
||||
[button setEnabled:NO];
|
||||
|
||||
_navigate_back_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_NAVIGATE_BACK_IDENTIFIER];
|
||||
[_navigate_back_toolbar_item setView:button];
|
||||
}
|
||||
|
||||
return _navigate_back_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)navigate_forward_toolbar_item
|
||||
{
|
||||
if (!_navigate_forward_toolbar_item) {
|
||||
auto* button = [self create_button:NSImageNameGoForwardTemplate
|
||||
with_action:@selector(navigateForward:)
|
||||
with_tooltip:@"Navigate forward"];
|
||||
[button setEnabled:NO];
|
||||
|
||||
_navigate_forward_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER];
|
||||
[_navigate_forward_toolbar_item setView:button];
|
||||
}
|
||||
|
||||
return _navigate_forward_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)reload_toolbar_item
|
||||
{
|
||||
if (!_reload_toolbar_item) {
|
||||
auto* button = [self create_button:NSImageNameRefreshTemplate
|
||||
with_action:@selector(reload:)
|
||||
with_tooltip:@"Reload page"];
|
||||
[button setEnabled:YES];
|
||||
|
||||
_reload_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_RELOAD_IDENTIFIER];
|
||||
[_reload_toolbar_item setView:button];
|
||||
}
|
||||
|
||||
return _reload_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)location_toolbar_item
|
||||
{
|
||||
if (!_location_toolbar_item) {
|
||||
auto* location_search_field = [[LocationSearchField alloc] init];
|
||||
[location_search_field setPlaceholderString:@"Enter web address"];
|
||||
[location_search_field setTextColor:[NSColor textColor]];
|
||||
[location_search_field setDelegate:self];
|
||||
|
||||
_location_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_LOCATION_IDENTIFIER];
|
||||
[_location_toolbar_item setView:location_search_field];
|
||||
}
|
||||
|
||||
return _location_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)zoom_toolbar_item
|
||||
{
|
||||
if (!_zoom_toolbar_item) {
|
||||
auto* button = [NSButton buttonWithTitle:@"100%"
|
||||
target:self
|
||||
action:@selector(resetZoom:)];
|
||||
[button setToolTip:@"Reset zoom level"];
|
||||
[button setHidden:YES];
|
||||
|
||||
_zoom_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_ZOOM_IDENTIFIER];
|
||||
[_zoom_toolbar_item setView:button];
|
||||
}
|
||||
|
||||
return _zoom_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)new_tab_toolbar_item
|
||||
{
|
||||
if (!_new_tab_toolbar_item) {
|
||||
auto* button = [self create_button:NSImageNameAddTemplate
|
||||
with_action:@selector(createNewTab:)
|
||||
with_tooltip:@"New tab"];
|
||||
|
||||
_new_tab_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_NEW_TAB_IDENTIFIER];
|
||||
[_new_tab_toolbar_item setView:button];
|
||||
}
|
||||
|
||||
return _new_tab_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSToolbarItem*)tab_overview_toolbar_item
|
||||
{
|
||||
if (!_tab_overview_toolbar_item) {
|
||||
auto* button = [self create_button:NSImageNameIconViewTemplate
|
||||
with_action:@selector(showTabOverview:)
|
||||
with_tooltip:@"Show all tabs"];
|
||||
|
||||
_tab_overview_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_TAB_OVERVIEW_IDENTIFIER];
|
||||
[_tab_overview_toolbar_item setView:button];
|
||||
}
|
||||
|
||||
return _tab_overview_toolbar_item;
|
||||
}
|
||||
|
||||
- (NSArray*)toolbar_identifiers
|
||||
{
|
||||
if (!_toolbar_identifiers) {
|
||||
_toolbar_identifiers = @[
|
||||
TOOLBAR_NAVIGATE_BACK_IDENTIFIER,
|
||||
TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER,
|
||||
NSToolbarFlexibleSpaceItemIdentifier,
|
||||
TOOLBAR_RELOAD_IDENTIFIER,
|
||||
TOOLBAR_LOCATION_IDENTIFIER,
|
||||
TOOLBAR_ZOOM_IDENTIFIER,
|
||||
NSToolbarFlexibleSpaceItemIdentifier,
|
||||
TOOLBAR_NEW_TAB_IDENTIFIER,
|
||||
TOOLBAR_TAB_OVERVIEW_IDENTIFIER,
|
||||
];
|
||||
}
|
||||
|
||||
return _toolbar_identifiers;
|
||||
}
|
||||
|
||||
#pragma mark - NSWindowController
|
||||
|
||||
- (IBAction)showWindow:(id)sender
|
||||
{
|
||||
self.window = self.parent
|
||||
? [[Tab alloc] initAsChild:self.parent pageIndex:m_page_index]
|
||||
: [[Tab alloc] init];
|
||||
|
||||
[self.window setDelegate:self];
|
||||
|
||||
[self.window setToolbar:self.toolbar];
|
||||
[self.window setToolbarStyle:NSWindowToolbarStyleUnified];
|
||||
|
||||
[self.window makeKeyAndOrderFront:sender];
|
||||
|
||||
[self focusLocationToolbarItem];
|
||||
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
[delegate setActiveTab:[self tab]];
|
||||
}
|
||||
|
||||
#pragma mark - NSWindowDelegate
|
||||
|
||||
- (void)windowDidBecomeMain:(NSNotification*)notification
|
||||
{
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
[delegate setActiveTab:[self tab]];
|
||||
}
|
||||
|
||||
- (void)windowWillClose:(NSNotification*)notification
|
||||
{
|
||||
[[self tab] tabWillClose];
|
||||
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
[delegate removeTab:self];
|
||||
}
|
||||
|
||||
- (void)windowDidMove:(NSNotification*)notification
|
||||
{
|
||||
auto position = Ladybird::ns_point_to_gfx_point([[self tab] frame].origin);
|
||||
[[[self tab] web_view] setWindowPosition:position];
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(NSNotification*)notification
|
||||
{
|
||||
if (self.location_toolbar_item_width != nil) {
|
||||
self.location_toolbar_item_width.active = NO;
|
||||
}
|
||||
|
||||
auto width = [self window].frame.size.width * 0.6;
|
||||
self.location_toolbar_item_width = [[[self.location_toolbar_item view] widthAnchor] constraintEqualToConstant:width];
|
||||
self.location_toolbar_item_width.active = YES;
|
||||
|
||||
[[[self tab] web_view] handleResize];
|
||||
}
|
||||
|
||||
- (void)windowDidChangeBackingProperties:(NSNotification*)notification
|
||||
{
|
||||
[[[self tab] web_view] handleDevicePixelRatioChange];
|
||||
}
|
||||
|
||||
- (BOOL)validateMenuItem:(NSMenuItem*)item
|
||||
{
|
||||
if ([item action] == @selector(toggleLineBoxBorders:)) {
|
||||
[item setState:m_settings.should_show_line_box_borders ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(toggleScripting:)) {
|
||||
[item setState:m_settings.scripting_enabled ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(togglePopupBlocking:)) {
|
||||
[item setState:m_settings.block_popups ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(toggleSameOriginPolicy:)) {
|
||||
[item setState:m_settings.same_origin_policy_enabled ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setUserAgentSpoof:)) {
|
||||
[item setState:(m_settings.user_agent_name == [[item title] UTF8String]) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(setNavigatorCompatibilityMode:)) {
|
||||
[item setState:(m_settings.navigator_compatibility_mode == [[[item title] lowercaseString] UTF8String]) ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
} else if ([item action] == @selector(toggleAutoplay:)) {
|
||||
[item setState:m_settings.autoplay_enabled ? NSControlStateValueOn : NSControlStateValueOff];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - NSToolbarDelegate
|
||||
|
||||
- (NSToolbarItem*)toolbar:(NSToolbar*)toolbar
|
||||
itemForItemIdentifier:(NSString*)identifier
|
||||
willBeInsertedIntoToolbar:(BOOL)flag
|
||||
{
|
||||
if ([identifier isEqual:TOOLBAR_NAVIGATE_BACK_IDENTIFIER]) {
|
||||
return self.navigate_back_toolbar_item;
|
||||
}
|
||||
if ([identifier isEqual:TOOLBAR_NAVIGATE_FORWARD_IDENTIFIER]) {
|
||||
return self.navigate_forward_toolbar_item;
|
||||
}
|
||||
if ([identifier isEqual:TOOLBAR_RELOAD_IDENTIFIER]) {
|
||||
return self.reload_toolbar_item;
|
||||
}
|
||||
if ([identifier isEqual:TOOLBAR_LOCATION_IDENTIFIER]) {
|
||||
return self.location_toolbar_item;
|
||||
}
|
||||
if ([identifier isEqual:TOOLBAR_ZOOM_IDENTIFIER]) {
|
||||
return self.zoom_toolbar_item;
|
||||
}
|
||||
if ([identifier isEqual:TOOLBAR_NEW_TAB_IDENTIFIER]) {
|
||||
return self.new_tab_toolbar_item;
|
||||
}
|
||||
if ([identifier isEqual:TOOLBAR_TAB_OVERVIEW_IDENTIFIER]) {
|
||||
return self.tab_overview_toolbar_item;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray*)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
|
||||
{
|
||||
return self.toolbar_identifiers;
|
||||
}
|
||||
|
||||
- (NSArray*)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
|
||||
{
|
||||
return self.toolbar_identifiers;
|
||||
}
|
||||
|
||||
#pragma mark - NSSearchFieldDelegate
|
||||
|
||||
- (BOOL)control:(NSControl*)control
|
||||
textView:(NSTextView*)text_view
|
||||
doCommandBySelector:(SEL)selector
|
||||
{
|
||||
if (selector != @selector(insertNewline:)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
auto url_string = Ladybird::ns_string_to_string([[text_view textStorage] string]);
|
||||
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
|
||||
|
||||
if (auto url = WebView::sanitize_url(url_string, [delegate searchEngine].query_url); url.has_value()) {
|
||||
[self loadURL:*url];
|
||||
}
|
||||
|
||||
[self.window makeFirstResponder:nil];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)controlTextDidEndEditing:(NSNotification*)notification
|
||||
{
|
||||
auto* location_search_field = (LocationSearchField*)[self.location_toolbar_item view];
|
||||
|
||||
auto url_string = Ladybird::ns_string_to_string([location_search_field stringValue]);
|
||||
[self setLocationFieldText:url_string];
|
||||
}
|
||||
|
||||
@end
|
||||
18
UI/AppKit/Interface/TaskManager.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Interface/LadybirdWebViewWindow.h>
|
||||
|
||||
@class LadybirdWebView;
|
||||
|
||||
@interface TaskManager : LadybirdWebViewWindow
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
@end
|
||||
66
UI/AppKit/Interface/TaskManager.mm
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/String.h>
|
||||
#include <LibCore/Timer.h>
|
||||
#include <LibWebView/Application.h>
|
||||
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/TaskManager.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
static constexpr CGFloat const WINDOW_WIDTH = 600;
|
||||
static constexpr CGFloat const WINDOW_HEIGHT = 400;
|
||||
|
||||
@interface TaskManager ()
|
||||
{
|
||||
RefPtr<Core::Timer> m_update_timer;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TaskManager
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
auto tab_rect = [[NSApp keyWindow] frame];
|
||||
auto position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2;
|
||||
auto position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2;
|
||||
auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
|
||||
if (self = [super initWithWebView:nil windowRect:window_rect]) {
|
||||
__weak TaskManager* weak_self = self;
|
||||
|
||||
m_update_timer = Core::Timer::create_repeating(1000, [weak_self] {
|
||||
TaskManager* strong_self = weak_self;
|
||||
if (strong_self == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
[strong_self updateStatistics];
|
||||
});
|
||||
|
||||
[self setContentView:self.web_view];
|
||||
[self setTitle:@"Task Manager"];
|
||||
[self setIsVisible:YES];
|
||||
|
||||
[self updateStatistics];
|
||||
m_update_timer->start();
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateStatistics
|
||||
{
|
||||
WebView::Application::the().update_process_statistics();
|
||||
[self.web_view loadHTML:WebView::Application::the().generate_process_statistics_html()];
|
||||
}
|
||||
|
||||
@end
|
||||
46
UI/AppKit/Interface/TaskManager.swift
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
||||
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import Ladybird.WebView
|
||||
import Ladybird.WebViewApplication
|
||||
import Ladybird.WebViewWindow
|
||||
import SwiftUI
|
||||
|
||||
public class TaskManager: LadybirdWebViewWindow {
|
||||
|
||||
private let WINDOW_WIDTH: CGFloat = 600
|
||||
private let WINDOW_HEIGHT: CGFloat = 400
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
init() {
|
||||
let tab_rect = NSApplication.shared.keyWindow!.frame
|
||||
let position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2
|
||||
let position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2
|
||||
let window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT)
|
||||
|
||||
super.init(webView: nil, windowRect: window_rect)
|
||||
|
||||
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
|
||||
if let strong_self = self {
|
||||
strong_self.updateStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
self.contentView = self.web_view
|
||||
self.title = "Task Manager"
|
||||
self.setIsVisible(true)
|
||||
|
||||
self.updateStatistics()
|
||||
}
|
||||
|
||||
func updateStatistics() {
|
||||
WebView.Application.the().update_process_statistics()
|
||||
self.web_view.loadHTML(WebView.Application.the().generate_process_statistics_html().__bytes_as_string_viewUnsafe())
|
||||
}
|
||||
}
|
||||
21
UI/AppKit/Interface/TaskManagerController.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@protocol TaskManagerDelegate <NSObject>
|
||||
|
||||
- (void)onTaskManagerClosed;
|
||||
|
||||
@end
|
||||
|
||||
@interface TaskManagerController : NSWindowController
|
||||
|
||||
- (instancetype)initWithDelegate:(id<TaskManagerDelegate>)delegate;
|
||||
|
||||
@end
|
||||
65
UI/AppKit/Interface/TaskManagerController.mm
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#import <Interface/LadybirdWebView.h>
|
||||
#import <Interface/TaskManager.h>
|
||||
#import <Interface/TaskManagerController.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
@interface TaskManagerController () <NSWindowDelegate>
|
||||
|
||||
@property (nonatomic, weak) id<TaskManagerDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TaskManagerController
|
||||
|
||||
- (instancetype)initWithDelegate:(id<TaskManagerDelegate>)delegate
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.delegate = delegate;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (TaskManager*)taskManager
|
||||
{
|
||||
return (TaskManager*)[self window];
|
||||
}
|
||||
|
||||
#pragma mark - NSWindowController
|
||||
|
||||
- (IBAction)showWindow:(id)sender
|
||||
{
|
||||
self.window = [[TaskManager alloc] init];
|
||||
[self.window setDelegate:self];
|
||||
[self.window makeKeyAndOrderFront:sender];
|
||||
}
|
||||
|
||||
#pragma mark - NSWindowDelegate
|
||||
|
||||
- (void)windowWillClose:(NSNotification*)notification
|
||||
{
|
||||
[self.delegate onTaskManagerClosed];
|
||||
}
|
||||
|
||||
- (void)windowDidResize:(NSNotification*)notification
|
||||
{
|
||||
[[[self taskManager] web_view] handleResize];
|
||||
}
|
||||
|
||||
- (void)windowDidChangeBackingProperties:(NSNotification*)notification
|
||||
{
|
||||
[[[self taskManager] web_view] handleDevicePixelRatioChange];
|
||||
}
|
||||
|
||||
@end
|
||||
50
UI/AppKit/Interface/TaskManagerController.swift
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@objc
|
||||
public protocol TaskManagerDelegate where Self: NSObject {
|
||||
func onTaskManagerClosed()
|
||||
}
|
||||
|
||||
public class TaskManagerController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
private weak var delegate: TaskManagerDelegate?
|
||||
|
||||
@objc
|
||||
public convenience init(delegate: TaskManagerDelegate) {
|
||||
self.init()
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
@IBAction public override func showWindow(_ sender: Any?) {
|
||||
self.window = TaskManager.init()
|
||||
self.window!.delegate = self
|
||||
self.window!.makeKeyAndOrderFront(sender)
|
||||
}
|
||||
|
||||
public func windowWillClose(_ sender: Notification) {
|
||||
self.delegate?.onTaskManagerClosed()
|
||||
}
|
||||
|
||||
public func windowDidResize(_ sender: Notification) {
|
||||
guard self.window != nil else { return }
|
||||
if !self.window!.inLiveResize {
|
||||
self.taskManager().web_view.handleResize()
|
||||
}
|
||||
}
|
||||
|
||||
public func windowDidChangeBackingProperties(_ sender: Notification) {
|
||||
self.taskManager().web_view.handleDevicePixelRatioChange()
|
||||
}
|
||||
|
||||
private func taskManager() -> TaskManager {
|
||||
return self.window as! TaskManager
|
||||
}
|
||||
}
|
||||
43
UI/AppKit/Utilities/Conversions.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibGfx/Color.h>
|
||||
#include <LibGfx/Point.h>
|
||||
#include <LibGfx/Rect.h>
|
||||
#include <LibGfx/Size.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
String ns_string_to_string(NSString*);
|
||||
ByteString ns_string_to_byte_string(NSString*);
|
||||
NSString* string_to_ns_string(StringView);
|
||||
|
||||
NSData* string_to_ns_data(StringView);
|
||||
|
||||
NSDictionary* deserialize_json_to_dictionary(StringView);
|
||||
|
||||
Gfx::IntRect ns_rect_to_gfx_rect(NSRect);
|
||||
NSRect gfx_rect_to_ns_rect(Gfx::IntRect);
|
||||
|
||||
Gfx::IntSize ns_size_to_gfx_size(NSSize);
|
||||
NSSize gfx_size_to_ns_size(Gfx::IntSize);
|
||||
|
||||
Gfx::IntPoint ns_point_to_gfx_point(NSPoint);
|
||||
NSPoint gfx_point_to_ns_point(Gfx::IntPoint);
|
||||
|
||||
Gfx::Color ns_color_to_gfx_color(NSColor*);
|
||||
NSColor* gfx_color_to_ns_color(Gfx::Color);
|
||||
|
||||
Gfx::IntPoint compute_origin_relative_to_window(NSWindow*, Gfx::IntPoint);
|
||||
|
||||
}
|
||||
131
UI/AppKit/Utilities/Conversions.mm
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#import <Utilities/Conversions.h>
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
String ns_string_to_string(NSString* string)
|
||||
{
|
||||
auto const* utf8 = [string UTF8String];
|
||||
return MUST(String::from_utf8({ utf8, strlen(utf8) }));
|
||||
}
|
||||
|
||||
ByteString ns_string_to_byte_string(NSString* string)
|
||||
{
|
||||
auto const* utf8 = [string UTF8String];
|
||||
return ByteString(utf8, strlen(utf8));
|
||||
}
|
||||
|
||||
NSString* string_to_ns_string(StringView string)
|
||||
{
|
||||
return [[NSString alloc] initWithData:string_to_ns_data(string) encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
NSData* string_to_ns_data(StringView string)
|
||||
{
|
||||
return [NSData dataWithBytes:string.characters_without_null_termination() length:string.length()];
|
||||
}
|
||||
|
||||
NSDictionary* deserialize_json_to_dictionary(StringView json)
|
||||
{
|
||||
auto* ns_json = string_to_ns_string(json);
|
||||
auto* json_data = [ns_json dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSError* error = nil;
|
||||
NSDictionary* dictionary = [NSJSONSerialization JSONObjectWithData:json_data
|
||||
options:0
|
||||
error:&error];
|
||||
|
||||
if (!dictionary) {
|
||||
NSLog(@"Error deserializing DOM tree: %@", error);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
Gfx::IntRect ns_rect_to_gfx_rect(NSRect rect)
|
||||
{
|
||||
return {
|
||||
static_cast<int>(rect.origin.x),
|
||||
static_cast<int>(rect.origin.y),
|
||||
static_cast<int>(rect.size.width),
|
||||
static_cast<int>(rect.size.height),
|
||||
};
|
||||
}
|
||||
|
||||
NSRect gfx_rect_to_ns_rect(Gfx::IntRect rect)
|
||||
{
|
||||
return NSMakeRect(
|
||||
static_cast<CGFloat>(rect.x()),
|
||||
static_cast<CGFloat>(rect.y()),
|
||||
static_cast<CGFloat>(rect.width()),
|
||||
static_cast<CGFloat>(rect.height()));
|
||||
}
|
||||
|
||||
Gfx::IntSize ns_size_to_gfx_size(NSSize size)
|
||||
{
|
||||
return {
|
||||
static_cast<int>(size.width),
|
||||
static_cast<int>(size.height),
|
||||
};
|
||||
}
|
||||
|
||||
NSSize gfx_size_to_ns_size(Gfx::IntSize size)
|
||||
{
|
||||
return NSMakeSize(
|
||||
static_cast<CGFloat>(size.width()),
|
||||
static_cast<CGFloat>(size.height()));
|
||||
}
|
||||
|
||||
Gfx::IntPoint ns_point_to_gfx_point(NSPoint point)
|
||||
{
|
||||
return {
|
||||
static_cast<int>(point.x),
|
||||
static_cast<int>(point.y),
|
||||
};
|
||||
}
|
||||
|
||||
NSPoint gfx_point_to_ns_point(Gfx::IntPoint point)
|
||||
{
|
||||
return NSMakePoint(
|
||||
static_cast<CGFloat>(point.x()),
|
||||
static_cast<CGFloat>(point.y()));
|
||||
}
|
||||
|
||||
Gfx::Color ns_color_to_gfx_color(NSColor* color)
|
||||
{
|
||||
auto rgb_color = [color colorUsingColorSpace:NSColorSpace.genericRGBColorSpace];
|
||||
if (rgb_color != nil)
|
||||
return {
|
||||
static_cast<u8>([rgb_color redComponent] * 255),
|
||||
static_cast<u8>([rgb_color greenComponent] * 255),
|
||||
static_cast<u8>([rgb_color blueComponent] * 255),
|
||||
static_cast<u8>([rgb_color alphaComponent] * 255)
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
NSColor* gfx_color_to_ns_color(Gfx::Color color)
|
||||
{
|
||||
return [NSColor colorWithRed:(color.red() / 255.f)
|
||||
green:(color.green() / 255.f)
|
||||
blue:(color.blue() / 255.f)
|
||||
alpha:(color.alpha() / 255.f)];
|
||||
}
|
||||
|
||||
Gfx::IntPoint compute_origin_relative_to_window(NSWindow* window, Gfx::IntPoint position)
|
||||
{
|
||||
// The origin of the NSWindow is its bottom-left corner, relative to the bottom-left of the screen. We must convert
|
||||
// window positions sent to/from WebContent between this origin and the window's and screen's top-left corners.
|
||||
auto screen_frame = Ladybird::ns_rect_to_gfx_rect([[window screen] frame]);
|
||||
auto window_frame = Ladybird::ns_rect_to_gfx_rect([window frame]);
|
||||
|
||||
position.set_y(screen_frame.height() - window_frame.height() - position.y());
|
||||
return position;
|
||||
}
|
||||
|
||||
}
|
||||
94
UI/AppKit/main.mm
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Enumerate.h>
|
||||
#include <LibGfx/Font/FontDatabase.h>
|
||||
#include <LibMain/Main.h>
|
||||
#include <LibWebView/Application.h>
|
||||
#include <LibWebView/ChromeProcess.h>
|
||||
#include <LibWebView/URL.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
#include <LibWebView/WebContentClient.h>
|
||||
#include <UI/DefaultSettings.h>
|
||||
#include <UI/MachPortServer.h>
|
||||
#include <UI/Utilities.h>
|
||||
|
||||
#import <Application/Application.h>
|
||||
#import <Application/ApplicationDelegate.h>
|
||||
#import <Application/EventLoopImplementation.h>
|
||||
#import <Interface/Tab.h>
|
||||
#import <Interface/TabController.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
static void open_urls_from_client(Vector<URL::URL> const& urls, WebView::NewWindow new_window)
|
||||
{
|
||||
ApplicationDelegate* delegate = [NSApp delegate];
|
||||
Tab* tab = new_window == WebView::NewWindow::Yes ? nil : [delegate activeTab];
|
||||
|
||||
for (auto [i, url] : enumerate(urls)) {
|
||||
auto activate_tab = i == 0 ? Web::HTML::ActivateTab::Yes : Web::HTML::ActivateTab::No;
|
||||
|
||||
auto* controller = [delegate createNewTab:url
|
||||
fromTab:tab
|
||||
activateTab:activate_tab];
|
||||
|
||||
tab = (Tab*)[controller window];
|
||||
}
|
||||
}
|
||||
|
||||
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||
{
|
||||
AK::set_rich_debug_enabled(true);
|
||||
|
||||
Application* application = [Application sharedApplication];
|
||||
|
||||
Core::EventLoopManager::install(*new Ladybird::CFEventLoopManager);
|
||||
[application setupWebViewApplication:arguments newTabPageURL:Browser::default_new_tab_url];
|
||||
|
||||
platform_init();
|
||||
|
||||
WebView::ChromeProcess chrome_process;
|
||||
|
||||
if (auto const& chrome_options = WebView::Application::chrome_options(); chrome_options.force_new_process == WebView::ForceNewProcess::No) {
|
||||
auto disposition = TRY(chrome_process.connect(chrome_options.raw_urls, chrome_options.new_window));
|
||||
|
||||
if (disposition == WebView::ChromeProcess::ProcessDisposition::ExitProcess) {
|
||||
outln("Opening in existing process");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
chrome_process.on_new_tab = [&](auto const& raw_urls) {
|
||||
open_urls_from_client(raw_urls, WebView::NewWindow::No);
|
||||
};
|
||||
|
||||
chrome_process.on_new_window = [&](auto const& raw_urls) {
|
||||
open_urls_from_client(raw_urls, WebView::NewWindow::Yes);
|
||||
};
|
||||
|
||||
auto mach_port_server = make<Ladybird::MachPortServer>();
|
||||
set_mach_server_name(mach_port_server->server_port_name());
|
||||
mach_port_server->on_receive_child_mach_port = [&](auto pid, auto port) {
|
||||
WebView::Application::the().set_process_mach_port(pid, move(port));
|
||||
};
|
||||
mach_port_server->on_receive_backing_stores = [](Ladybird::MachPortServer::BackingStoresMessage message) {
|
||||
if (auto view = WebView::WebContentClient::view_for_pid_and_page_id(message.pid, message.page_id); view.has_value())
|
||||
view->did_allocate_iosurface_backing_stores(message.front_backing_store_id, move(message.front_backing_store_port), message.back_backing_store_id, move(message.back_backing_store_port));
|
||||
};
|
||||
|
||||
// FIXME: Create an abstraction to re-spawn the RequestServer and re-hook up its client hooks to each tab on crash
|
||||
TRY([application launchRequestServer]);
|
||||
|
||||
TRY([application launchImageDecoder]);
|
||||
|
||||
auto* delegate = [[ApplicationDelegate alloc] init];
|
||||
[NSApp setDelegate:delegate];
|
||||
|
||||
return WebView::Application::the().execute();
|
||||
}
|
||||
19
UI/AppKit/module.modulemap
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module Ladybird [system] {
|
||||
requires cplusplus
|
||||
requires objc_arc
|
||||
|
||||
explicit module WebView {
|
||||
header "Interface/LadybirdWebView.h"
|
||||
export *
|
||||
}
|
||||
|
||||
explicit module WebViewWindow {
|
||||
header "Interface/LadybirdWebViewWindow.h"
|
||||
export *
|
||||
}
|
||||
|
||||
explicit module WebViewApplication {
|
||||
header "../../Libraries/LibWebView/Application.h"
|
||||
export *
|
||||
}
|
||||
}
|
||||
149
UI/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
include(cmake/ResourceFiles.cmake)
|
||||
|
||||
set(LADYBIRD_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/HelperProcess.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Utilities.cpp
|
||||
)
|
||||
set(LADYBIRD_HEADERS
|
||||
HelperProcess.h
|
||||
Utilities.h
|
||||
)
|
||||
|
||||
function(create_ladybird_bundle target_name)
|
||||
set_target_properties(${target_name} PROPERTIES
|
||||
OUTPUT_NAME "Ladybird"
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER org.ladybird.Ladybird
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
||||
MACOSX_BUNDLE_INFO_PLIST "${LADYBIRD_SOURCE_DIR}/UI/Info.plist"
|
||||
MACOSX_BUNDLE TRUE
|
||||
WIN32_EXECUTABLE TRUE
|
||||
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER org.ladybird.Ladybird
|
||||
)
|
||||
|
||||
if (APPLE)
|
||||
set(bundle_dir "$<TARGET_BUNDLE_DIR:${target_name}>")
|
||||
add_custom_command(TARGET ${target_name} POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E make_directory "${bundle_dir}/Contents/Resources"
|
||||
COMMAND "iconutil" --convert icns "${LADYBIRD_SOURCE_DIR}/UI/Icons/macos/app_icon.iconset" --output "${bundle_dir}/Contents/Resources/app_icon.icns"
|
||||
)
|
||||
|
||||
# Note: This symlink is removed in the install commands
|
||||
# This makes the bundle in the build directory *NOT* relocatable
|
||||
add_custom_command(TARGET ${target_name} POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E create_symlink "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}" "${bundle_dir}/Contents/lib"
|
||||
)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE MATCHES "Release|RelWithDebInfo" AND "arm64" IN_LIST CMAKE_OSX_ARCHITECTURES)
|
||||
add_custom_command(TARGET ${target_name} POST_BUILD
|
||||
COMMAND codesign -s - -v -f --entitlements "${LADYBIRD_SOURCE_DIR}/Meta/debug.plist" "${bundle_dir}"
|
||||
)
|
||||
else()
|
||||
add_custom_target(apply-debug-entitlements
|
||||
COMMAND codesign -s - -v -f --entitlements "${LADYBIRD_SOURCE_DIR}/Meta/debug.plist" "${bundle_dir}"
|
||||
USES_TERMINAL
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
set(resource_base_dir "$<TARGET_BUNDLE_DIR:${target_name}>/Contents/Resources")
|
||||
else()
|
||||
set(resource_base_dir "${CMAKE_BINARY_DIR}/${IN_BUILD_PREFIX}${CMAKE_INSTALL_DATADIR}/Lagom")
|
||||
endif()
|
||||
|
||||
copy_resources_to_build(${resource_base_dir} ${target_name})
|
||||
endfunction()
|
||||
|
||||
# Select UI Framework
|
||||
if (ENABLE_QT)
|
||||
add_subdirectory(Qt)
|
||||
elseif (APPLE)
|
||||
add_subdirectory(AppKit)
|
||||
elseif(ANDROID)
|
||||
add_subdirectory(Android)
|
||||
else()
|
||||
# TODO: Check for other GUI frameworks here when we move them in-tree
|
||||
# For now, we can export a static library of common files for chromes to link to
|
||||
add_library(ladybird STATIC ${LADYBIRD_SOURCES})
|
||||
endif()
|
||||
|
||||
if (NOT TARGET ladybird)
|
||||
message(FATAL_ERROR "UI Framework selection must declare a ladybird target")
|
||||
endif()
|
||||
|
||||
if (APPLE)
|
||||
target_sources(ladybird PRIVATE MachPortServer.cpp)
|
||||
target_link_libraries(ladybird PRIVATE LibThreading)
|
||||
endif()
|
||||
|
||||
if (ENABLE_INSTALL_HEADERS)
|
||||
target_sources(ladybird PUBLIC FILE_SET ladybird TYPE HEADERS
|
||||
BASE_DIRS ${LADYBIRD_SOURCE_DIR}
|
||||
FILES ${LADYBIRD_HEADERS}
|
||||
)
|
||||
endif()
|
||||
|
||||
if (TARGET ladybird_impl)
|
||||
set(LADYBIRD_TARGET ladybird_impl PUBLIC)
|
||||
else()
|
||||
set(LADYBIRD_TARGET ladybird PRIVATE)
|
||||
endif()
|
||||
|
||||
set(LADYBIRD_LIBS AK LibCore LibFileSystem LibGfx LibImageDecoderClient LibIPC LibJS LibMain LibWeb LibWebView LibRequests LibURL)
|
||||
target_link_libraries(${LADYBIRD_TARGET} PRIVATE ${LADYBIRD_LIBS})
|
||||
|
||||
target_include_directories(${LADYBIRD_TARGET} ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_include_directories(${LADYBIRD_TARGET} ${LADYBIRD_SOURCE_DIR})
|
||||
target_include_directories(${LADYBIRD_TARGET} ${LADYBIRD_SOURCE_DIR}/Services/)
|
||||
|
||||
function(set_helper_process_properties)
|
||||
set(targets ${ARGV})
|
||||
if (APPLE)
|
||||
# Store helper processes in the same bundle directory as the main application
|
||||
set_target_properties(${targets} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<TARGET_FILE_DIR:ladybird>")
|
||||
else()
|
||||
set_target_properties(${targets} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${IN_BUILD_PREFIX}${CMAKE_INSTALL_LIBEXECDIR}")
|
||||
|
||||
if (NOT CMAKE_INSTALL_LIBEXECDIR STREQUAL "libexec")
|
||||
set_source_files_properties(Utilities.cpp PROPERTIES COMPILE_DEFINITIONS LADYBIRD_LIBEXECDIR="${CMAKE_INSTALL_LIBEXECDIR}")
|
||||
set_source_files_properties(Utilities.cpp TARGET_DIRECTORY ladybird PROPERTIES COMPILE_DEFINITIONS LADYBIRD_LIBEXECDIR="${CMAKE_INSTALL_LIBEXECDIR}")
|
||||
set_source_files_properties(Utilities.cpp TARGET_DIRECTORY ${targets} PROPERTIES COMPILE_DEFINITIONS LADYBIRD_LIBEXECDIR="${CMAKE_INSTALL_LIBEXECDIR}")
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_custom_target(run
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "LADYBIRD_SOURCE_DIR=${LADYBIRD_SOURCE_DIR}" "$<TARGET_FILE:ladybird>" $ENV{LAGOM_ARGS}
|
||||
USES_TERMINAL
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
if (APPLE)
|
||||
add_custom_target(debug-ladybird
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "LADYBIRD_SOURCE_DIR=${LADYBIRD_SOURCE_DIR}" lldb "$<TARGET_BUNDLE_DIR:ladybird>"
|
||||
USES_TERMINAL
|
||||
)
|
||||
else()
|
||||
add_custom_target(debug-ladybird
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "LADYBIRD_SOURCE_DIR=${LADYBIRD_SOURCE_DIR}" gdb "$<TARGET_FILE:ladybird>"
|
||||
USES_TERMINAL
|
||||
)
|
||||
endif()
|
||||
|
||||
add_subdirectory(Headless)
|
||||
|
||||
set(ladybird_helper_processes ImageDecoder RequestServer WebContent WebWorker)
|
||||
|
||||
add_dependencies(ladybird ${ladybird_helper_processes})
|
||||
add_dependencies(headless-browser ${ladybird_helper_processes})
|
||||
add_dependencies(WebDriver ladybird headless-browser)
|
||||
|
||||
set_helper_process_properties(${ladybird_helper_processes})
|
||||
if (APPLE)
|
||||
set_helper_process_properties(headless-browser WebDriver)
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_SKIP_INSTALL_RULES)
|
||||
include(cmake/InstallRules.cmake)
|
||||
endif()
|
||||
21
UI/DefaultSettings.h
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/StringView.h>
|
||||
|
||||
namespace Browser {
|
||||
|
||||
static constexpr StringView default_homepage_url = "resource://html/misc/welcome.html"sv;
|
||||
static constexpr StringView default_new_tab_url = "about:newtab"sv;
|
||||
static constexpr StringView default_color_scheme = "auto"sv;
|
||||
static constexpr bool default_enable_content_filters = true;
|
||||
static constexpr bool default_show_bookmarks_bar = true;
|
||||
static constexpr bool default_close_download_widget_on_finish = false;
|
||||
static constexpr bool default_allow_autoplay_on_all_websites = false;
|
||||
|
||||
}
|
||||
217
UI/FontPlugin.cpp
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/TypeCasts.h>
|
||||
#include <LibCore/Resource.h>
|
||||
#include <LibCore/StandardPaths.h>
|
||||
#include <LibGfx/Font/FontDatabase.h>
|
||||
#include <LibGfx/Font/PathFontProvider.h>
|
||||
#include <UI/FontPlugin.h>
|
||||
|
||||
#ifdef USE_FONTCONFIG
|
||||
# include <fontconfig/fontconfig.h>
|
||||
#endif
|
||||
|
||||
namespace Ladybird {
|
||||
|
||||
FontPlugin::FontPlugin(bool is_layout_test_mode, Gfx::SystemFontProvider* font_provider)
|
||||
: m_is_layout_test_mode(is_layout_test_mode)
|
||||
{
|
||||
#ifdef USE_FONTCONFIG
|
||||
{
|
||||
auto fontconfig_initialized = FcInit();
|
||||
VERIFY(fontconfig_initialized);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!font_provider)
|
||||
font_provider = &static_cast<Gfx::PathFontProvider&>(Gfx::FontDatabase::the().install_system_font_provider(make<Gfx::PathFontProvider>()));
|
||||
if (is<Gfx::PathFontProvider>(*font_provider)) {
|
||||
auto& path_font_provider = static_cast<Gfx::PathFontProvider&>(*font_provider);
|
||||
// Load anything we can find in the system's font directories
|
||||
for (auto const& path : Core::StandardPaths::font_directories().release_value_but_fixme_should_propagate_errors())
|
||||
path_font_provider.load_all_fonts_from_uri(MUST(String::formatted("file://{}", path)));
|
||||
}
|
||||
|
||||
update_generic_fonts();
|
||||
|
||||
auto default_font_name = generic_font_name(Web::Platform::GenericFont::UiSansSerif);
|
||||
m_default_font = Gfx::FontDatabase::the().get(default_font_name, 12.0, 400, Gfx::FontWidth::Normal, 0);
|
||||
VERIFY(m_default_font);
|
||||
|
||||
auto default_fixed_width_font_name = generic_font_name(Web::Platform::GenericFont::UiMonospace);
|
||||
m_default_fixed_width_font = Gfx::FontDatabase::the().get(default_fixed_width_font_name, 12.0, 400, Gfx::FontWidth::Normal, 0);
|
||||
VERIFY(m_default_fixed_width_font);
|
||||
}
|
||||
|
||||
FontPlugin::~FontPlugin() = default;
|
||||
|
||||
Gfx::Font& FontPlugin::default_font()
|
||||
{
|
||||
return *m_default_font;
|
||||
}
|
||||
|
||||
Gfx::Font& FontPlugin::default_fixed_width_font()
|
||||
{
|
||||
return *m_default_fixed_width_font;
|
||||
}
|
||||
|
||||
RefPtr<Gfx::Font> FontPlugin::default_emoji_font(float point_size)
|
||||
{
|
||||
FlyString default_emoji_font_name;
|
||||
|
||||
if (m_is_layout_test_mode) {
|
||||
default_emoji_font_name = "Noto Emoji"_fly_string;
|
||||
} else {
|
||||
#ifdef AK_OS_MACOS
|
||||
default_emoji_font_name = "Apple Color Emoji"_fly_string;
|
||||
#else
|
||||
default_emoji_font_name = "Noto Color Emoji"_fly_string;
|
||||
#endif
|
||||
}
|
||||
|
||||
return Gfx::FontDatabase::the().get(default_emoji_font_name, point_size, 400, Gfx::FontWidth::Normal, 0);
|
||||
}
|
||||
|
||||
#ifdef USE_FONTCONFIG
|
||||
static Optional<String> query_fontconfig_for_generic_family(Web::Platform::GenericFont generic_font)
|
||||
{
|
||||
char const* pattern_string = nullptr;
|
||||
switch (generic_font) {
|
||||
case Web::Platform::GenericFont::Cursive:
|
||||
pattern_string = "cursive";
|
||||
break;
|
||||
case Web::Platform::GenericFont::Fantasy:
|
||||
pattern_string = "fantasy";
|
||||
break;
|
||||
case Web::Platform::GenericFont::Monospace:
|
||||
pattern_string = "monospace";
|
||||
break;
|
||||
case Web::Platform::GenericFont::SansSerif:
|
||||
pattern_string = "sans-serif";
|
||||
break;
|
||||
case Web::Platform::GenericFont::Serif:
|
||||
pattern_string = "serif";
|
||||
break;
|
||||
case Web::Platform::GenericFont::UiMonospace:
|
||||
pattern_string = "monospace";
|
||||
break;
|
||||
case Web::Platform::GenericFont::UiRounded:
|
||||
pattern_string = "sans-serif";
|
||||
break;
|
||||
case Web::Platform::GenericFont::UiSansSerif:
|
||||
pattern_string = "sans-serif";
|
||||
break;
|
||||
case Web::Platform::GenericFont::UiSerif:
|
||||
pattern_string = "serif";
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
auto* config = FcConfigGetCurrent();
|
||||
VERIFY(config);
|
||||
|
||||
FcPattern* pattern = FcNameParse(reinterpret_cast<FcChar8 const*>(pattern_string));
|
||||
VERIFY(pattern);
|
||||
|
||||
auto success = FcConfigSubstitute(config, pattern, FcMatchPattern);
|
||||
VERIFY(success);
|
||||
|
||||
FcDefaultSubstitute(pattern);
|
||||
|
||||
// Never select bitmap fonts.
|
||||
success = FcPatternAddBool(pattern, FC_SCALABLE, FcTrue);
|
||||
VERIFY(success);
|
||||
|
||||
// FIXME: Enable this once we can handle OpenType variable fonts.
|
||||
success = FcPatternAddBool(pattern, FC_VARIABLE, FcFalse);
|
||||
VERIFY(success);
|
||||
|
||||
Optional<String> name;
|
||||
FcResult result {};
|
||||
|
||||
if (auto* matched = FcFontMatch(config, pattern, &result)) {
|
||||
FcChar8* family = nullptr;
|
||||
if (FcPatternGetString(matched, FC_FAMILY, 0, &family) == FcResultMatch) {
|
||||
auto const* family_cstring = reinterpret_cast<char const*>(family);
|
||||
if (auto string = String::from_utf8(StringView { family_cstring, strlen(family_cstring) }); !string.is_error()) {
|
||||
name = string.release_value();
|
||||
}
|
||||
}
|
||||
FcPatternDestroy(matched);
|
||||
}
|
||||
FcPatternDestroy(pattern);
|
||||
return name;
|
||||
}
|
||||
#endif
|
||||
|
||||
void FontPlugin::update_generic_fonts()
|
||||
{
|
||||
// How we choose which system font to use for each CSS font:
|
||||
// 1. Try a list of known-suitable fonts with their names hard-coded below.
|
||||
|
||||
// This is rather weird, but it's how things work right now.
|
||||
// We should eventually have a way to query the system for the default font.
|
||||
// Furthermore, we should allow overriding via some kind of configuration mechanism.
|
||||
|
||||
m_generic_font_names.resize(static_cast<size_t>(Web::Platform::GenericFont::__Count));
|
||||
|
||||
auto update_mapping = [&](Web::Platform::GenericFont generic_font, ReadonlySpan<FlyString> fallbacks) {
|
||||
if (m_is_layout_test_mode) {
|
||||
m_generic_font_names[static_cast<size_t>(generic_font)] = "SerenitySans"_fly_string;
|
||||
return;
|
||||
}
|
||||
|
||||
RefPtr<Gfx::Font const> gfx_font;
|
||||
|
||||
#ifdef USE_FONTCONFIG
|
||||
auto name = query_fontconfig_for_generic_family(generic_font);
|
||||
if (name.has_value()) {
|
||||
gfx_font = Gfx::FontDatabase::the().get(name.value(), 16, 400, Gfx::FontWidth::Normal, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!gfx_font) {
|
||||
for (auto const& fallback : fallbacks) {
|
||||
gfx_font = Gfx::FontDatabase::the().get(fallback, 16, 400, Gfx::FontWidth::Normal, 0);
|
||||
if (gfx_font)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_generic_font_names[static_cast<size_t>(generic_font)] = gfx_font ? gfx_font->family() : String {};
|
||||
};
|
||||
|
||||
// Fallback fonts to look for if Gfx::Font can't load expected font
|
||||
// The lists are basically arbitrary, taken from https://www.w3.org/Style/Examples/007/fonts.en.html
|
||||
// (We also add Android-specific font names to the list from W3 where required.)
|
||||
Vector<FlyString> cursive_fallbacks { "Comic Sans MS"_fly_string, "Comic Sans"_fly_string, "Apple Chancery"_fly_string, "Bradley Hand"_fly_string, "Brush Script MT"_fly_string, "Snell Roundhand"_fly_string, "URW Chancery L"_fly_string, "Dancing Script"_fly_string };
|
||||
Vector<FlyString> fantasy_fallbacks { "Impact"_fly_string, "Luminari"_fly_string, "Chalkduster"_fly_string, "Jazz LET"_fly_string, "Blippo"_fly_string, "Stencil Std"_fly_string, "Marker Felt"_fly_string, "Trattatello"_fly_string, "Coming Soon"_fly_string };
|
||||
Vector<FlyString> monospace_fallbacks { "Andale Mono"_fly_string, "Courier New"_fly_string, "Courier"_fly_string, "FreeMono"_fly_string, "OCR A Std"_fly_string, "DejaVu Sans Mono"_fly_string, "Droid Sans Mono"_fly_string, "Liberation Mono"_fly_string };
|
||||
Vector<FlyString> sans_serif_fallbacks { "Arial"_fly_string, "Helvetica"_fly_string, "Verdana"_fly_string, "Trebuchet MS"_fly_string, "Gill Sans"_fly_string, "Noto Sans"_fly_string, "Avantgarde"_fly_string, "Optima"_fly_string, "Arial Narrow"_fly_string, "Liberation Sans"_fly_string, "Roboto"_fly_string };
|
||||
Vector<FlyString> serif_fallbacks { "Times"_fly_string, "Times New Roman"_fly_string, "Didot"_fly_string, "Georgia"_fly_string, "Palatino"_fly_string, "Bookman"_fly_string, "New Century Schoolbook"_fly_string, "American Typewriter"_fly_string, "Liberation Serif"_fly_string, "Roman"_fly_string, "Noto Serif"_fly_string };
|
||||
|
||||
update_mapping(Web::Platform::GenericFont::Cursive, cursive_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::Fantasy, fantasy_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::Monospace, monospace_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::SansSerif, sans_serif_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::Serif, serif_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::UiMonospace, monospace_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::UiRounded, sans_serif_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::UiSansSerif, sans_serif_fallbacks);
|
||||
update_mapping(Web::Platform::GenericFont::UiSerif, serif_fallbacks);
|
||||
}
|
||||
|
||||
FlyString FontPlugin::generic_font_name(Web::Platform::GenericFont generic_font)
|
||||
{
|
||||
return m_generic_font_names[static_cast<size_t>(generic_font)];
|
||||
}
|
||||
|
||||
}
|
||||