mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-21 23:20:20 +00:00
Compare commits
320 commits
bed45cfdd3
...
47b2bd93a3
Author | SHA1 | Date | |
---|---|---|---|
|
47b2bd93a3 | ||
|
34c64c64db | ||
|
260017133f | ||
|
001df24935 | ||
|
b60cb699a9 | ||
|
8d13115d9a | ||
|
cd14b215d1 | ||
|
35764db0b7 | ||
|
caf7983039 | ||
|
cdb54fe504 | ||
|
7f989765f5 | ||
|
41c172c663 | ||
|
ac5699c8fc | ||
|
ce26e5d757 | ||
|
3a2cc1aa20 | ||
|
0448d4d609 | ||
|
a1687854ab | ||
|
d1120e1809 | ||
|
248e4bb517 | ||
|
b4ba65c6e5 | ||
|
009f328308 | ||
|
d55caff227 | ||
|
570c5f8df2 | ||
|
d6bcd3fb0b | ||
|
d527c5df5d | ||
|
13f349aea2 | ||
|
84f673515b | ||
|
4e48298414 | ||
|
bf5cf720b5 | ||
|
488034477a | ||
|
f57ff63432 | ||
|
c715711f88 | ||
|
5689621c2b | ||
|
4742775262 | ||
|
a80523be18 | ||
|
55c81482b0 | ||
|
dfaa3bf649 | ||
|
5fe0d3352d | ||
|
eca378a7a3 | ||
|
e4e05837e1 | ||
|
c8d2404230 | ||
|
d368fcadac | ||
|
f7517c5b8d | ||
|
b94307583b | ||
|
e236f1d2ae | ||
|
63a5717bc7 | ||
|
c5afe58540 | ||
|
3bcd91b109 | ||
|
7d1291b9f0 | ||
|
6911c45bab | ||
|
879ae94183 | ||
|
7e20f4726f | ||
|
7f72c28e78 | ||
|
d704b61066 | ||
|
b93d8ef875 | ||
|
8a07131229 | ||
|
063cd68bf4 | ||
|
f638f84185 | ||
|
4203b7823f | ||
|
cd446e5e9c | ||
|
ab0dc83d28 | ||
|
6fc06f45c2 | ||
|
e98e9b8e81 | ||
|
4b1deb6fe1 | ||
|
356507284e | ||
|
ec5ea0d686 | ||
|
b3c253e50f | ||
|
d0c0db5bb3 | ||
|
b99a3ec2df | ||
|
74b27d620d | ||
|
dabf3da7e5 | ||
|
11460b3daa | ||
|
43056a8684 | ||
|
a423493dd8 | ||
|
77d205571d | ||
|
f09ed59351 | ||
|
866609c682 | ||
|
3468a83e45 | ||
|
4a731b3858 | ||
|
ddd15e96b6 | ||
|
61d52c8a3f | ||
|
33e7d6121b | ||
|
d87144fde2 | ||
|
69f5f40617 | ||
|
a828a0e158 | ||
|
4b4a6991e3 | ||
|
fca6fd0b85 | ||
|
829391e714 | ||
|
726f2cfb11 | ||
|
32cf4d1e29 | ||
|
d5fb48a6f5 | ||
|
458167935c | ||
|
a95f761cb4 | ||
|
6033349574 | ||
|
ed409eacf5 | ||
|
6ffc7ea36d | ||
|
564dc0a434 | ||
|
24a6fd3d76 | ||
|
c47d19d05a | ||
|
a820308a02 | ||
|
b342758dbf | ||
|
801499f13e | ||
|
135daeb8bb | ||
|
be09893fa7 | ||
|
8bd394f349 | ||
|
ed76e1ed4b | ||
|
dd6acfecd4 | ||
|
f88826691c | ||
|
59e0b7ccb7 | ||
|
8cd514d83c | ||
|
7fe110225b | ||
|
96a35767b6 | ||
|
7b7bb60393 | ||
|
e28e4f6700 | ||
|
9f541c363d | ||
|
ed80e929e5 | ||
|
66530086a4 | ||
|
ff791a63fc | ||
|
3856dd946b | ||
|
c898ee90cf | ||
|
8d511b2f7b | ||
|
3e8c8b185e | ||
|
5bcba896c2 | ||
|
638a8aecad | ||
|
c0da3e356a | ||
|
35ba7c7e00 | ||
|
5a2260a0bc | ||
|
fd15910adf | ||
|
355ce72c06 | ||
|
b11fdea175 | ||
|
a91af764f6 | ||
|
077ae6efa1 | ||
|
6affbf78c2 | ||
|
fd0c63b338 | ||
|
e8c228fb93 | ||
|
634823d5b4 | ||
|
a4b43cae9a | ||
|
061ac1f8c7 | ||
|
5a4d657a4e | ||
|
b264d18ad1 | ||
|
00bc22c332 | ||
|
69c84d3f63 | ||
|
aa9ed71ff3 | ||
|
0339ece565 | ||
|
98dadb0ce6 | ||
|
8d93cac983 | ||
|
ab309dcc58 | ||
|
ed7ec7a0f8 | ||
|
13bd52249d | ||
|
c3783cf3bd | ||
|
63873f3809 | ||
|
2173219eac | ||
|
0b9c4b8adc | ||
|
596a4e55dd | ||
|
f949334a9a | ||
|
a59d9a3986 | ||
|
c0ae3aa884 | ||
|
e5d71a6c82 | ||
|
e21b5cab32 | ||
|
ae7ee22aea | ||
|
3f10a5701d | ||
|
ec5101a1d3 | ||
|
87fc7028d7 | ||
|
7c2601f315 | ||
|
6319dedbcd | ||
|
1383d03c02 | ||
|
1ea236e454 | ||
|
9812fac2c3 | ||
|
c998f22f9e | ||
|
e0adbf3ebb | ||
|
b895a135d5 | ||
|
137db0e38e | ||
|
71eded0471 | ||
|
079c28d5e6 | ||
|
f87041bf3a | ||
|
ce23efc5f6 | ||
|
70695e4fce | ||
|
02268e9c60 | ||
|
3ecc843cff | ||
|
77d30a0cb7 | ||
|
4c2d4cdf50 | ||
|
f7993495bd | ||
|
f8c853712e | ||
|
b290c180e0 | ||
|
a6e9f107eb | ||
|
d842d04be4 | ||
|
dc9179bb1b | ||
|
581597cb34 | ||
|
c747b1c6b5 | ||
|
3a71b8cda3 | ||
|
da31c10ce1 | ||
|
3a43fa9e35 | ||
|
7f803c5c3d | ||
|
ad1f93504e | ||
|
a94282e0e8 | ||
|
aacf9b08ed | ||
|
d95ae629ee | ||
|
300f212044 | ||
|
5aacb053a3 | ||
|
1a1fb14e26 | ||
|
d7caa426a0 | ||
|
5b67f17551 | ||
|
19ee8ddec2 | ||
|
1c77135948 | ||
|
d4f8b598cb | ||
|
c2988a7dd5 | ||
|
ae6d105f41 | ||
|
0bf2a8362a | ||
|
c0bcebeb08 | ||
|
1bcc6764ae | ||
|
5a1eb9e220 | ||
|
329cd946ac | ||
|
884a4163a0 | ||
|
f73a434177 | ||
|
4e1dab477a | ||
|
44d6601dc5 | ||
|
652dde5022 | ||
|
bb7dff7dfe | ||
|
1b38ebcc7f | ||
|
a3ef24e30a | ||
|
3b04c983f1 | ||
|
25c067872c | ||
|
ce56bc29e2 | ||
|
1e54003cb1 | ||
|
9b79a686eb | ||
|
2a5dbedad4 | ||
|
698dd600f2 | ||
|
1849eca503 | ||
|
957032809b | ||
|
83b1db785a | ||
|
c04b14d0cb | ||
|
c64521aa4f | ||
|
47543b7076 | ||
|
a6822986bb | ||
|
3e5476c9e0 | ||
|
213155ad7d | ||
|
4dd14d812f | ||
|
6df4e5f5e7 | ||
|
d3c21e4038 | ||
|
b08f12d3e6 | ||
|
3d8ab0e67c | ||
|
d63a979bde | ||
|
889e7942fa | ||
|
f2a1643650 | ||
|
2b09afb971 | ||
|
d53cb9833b | ||
|
6b7d5dbec6 | ||
|
5d0b206d6e | ||
|
b21857b265 | ||
|
e6ee7f3e64 | ||
|
e8d91f2234 | ||
|
3b1d1d4582 | ||
|
eef9a53eec | ||
|
d199bf60cf | ||
|
cf27eef583 | ||
|
2f6bcb3538 | ||
|
520aa04092 | ||
|
d0646236ca | ||
|
13b7c26e9f | ||
|
e094712e3a | ||
|
71ccaeda16 | ||
|
77a46ab1b8 | ||
|
b6a5b7e186 | ||
|
10724a7cb3 | ||
|
2fb3b6c542 | ||
|
faf6fd1189 | ||
|
70ce8046c3 | ||
|
4add737e88 | ||
|
d2151e444e | ||
|
956b279ae1 | ||
|
d796f609db | ||
|
dabd60180f | ||
|
55b19c3177 | ||
|
eed20ad951 | ||
|
dfd50afa4e | ||
|
6d29afaa6c | ||
|
b1587cc60f | ||
|
3ba7c53668 | ||
|
b3b97d2049 | ||
|
b397a0d535 | ||
|
6a6618f5ea | ||
|
e240084437 | ||
|
7efc89e92b | ||
|
a073e35562 | ||
|
a8c1d12e84 | ||
|
07d8ddb5fa | ||
|
676e54c397 | ||
|
20a78a42d6 | ||
|
bd4c29322c | ||
|
1bdc41faa1 | ||
|
566870b2bd | ||
|
d1c7c0ba19 | ||
|
ddc3017464 | ||
|
aa0811d24e | ||
|
d2306efaea | ||
|
0ff91a5273 | ||
|
a14937c45e | ||
|
9e1f001ffe | ||
|
bd50a31be6 | ||
|
7460f0c6e2 | ||
|
1e05457cd1 | ||
|
6862d33e7c | ||
|
b4e5afa8d5 | ||
|
9ee7d4d90a | ||
|
ea20545853 | ||
|
c1ec2ddb63 | ||
|
653c8f231d | ||
|
a1a740bb3e | ||
|
dd11d48a1d | ||
|
5aa1d7837f | ||
|
8c809fa5ee | ||
|
11458f0d91 | ||
|
1510c1876c | ||
|
08ae305dc5 | ||
|
68f58b23ce | ||
|
e02ca0480f | ||
|
db47cc41f8 | ||
|
93712b24bf | ||
|
950e819ee7 | ||
|
22e0eeada2 |
7576 changed files with 421954 additions and 416390 deletions
|
@ -46,7 +46,7 @@ Checks: >
|
||||||
-readability-uppercase-literal-suffix,
|
-readability-uppercase-literal-suffix,
|
||||||
-readability-use-anyofallof,
|
-readability-use-anyofallof,
|
||||||
WarningsAsErrors: ''
|
WarningsAsErrors: ''
|
||||||
HeaderFilterRegex: 'AK|Userland|Tests'
|
HeaderFilterRegex: 'AK|Libraries|Services|Tests|Utilities'
|
||||||
FormatStyle: none
|
FormatStyle: none
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
- key: bugprone-dangling-handle.HandleClasses
|
- key: bugprone-dangling-handle.HandleClasses
|
||||||
|
|
30
.github/CODEOWNERS
vendored
30
.github/CODEOWNERS
vendored
|
@ -1,16 +1,16 @@
|
||||||
/Lagom/Tools/CodeGenerators/LibWeb @AtkinsSJ
|
/Lagom/Tools/CodeGenerators/LibWeb @AtkinsSJ
|
||||||
/Userland/Libraries/LibCrypto @alimpfard
|
/Libraries/LibCrypto @alimpfard
|
||||||
/Userland/Libraries/LibHTTP @alimpfard
|
/Libraries/LibHTTP @alimpfard
|
||||||
/Userland/Libraries/LibJS/Runtime/Intl @trflynn89
|
/Libraries/LibJS/Runtime/Intl @trflynn89
|
||||||
/Userland/Libraries/LibRegex @alimpfard
|
/Libraries/LibRegex @alimpfard
|
||||||
/Userland/Libraries/LibTLS @alimpfard
|
/Libraries/LibTLS @alimpfard
|
||||||
/Userland/Libraries/LibTimeZone @trflynn89
|
/Libraries/LibTimeZone @trflynn89
|
||||||
/Userland/Libraries/LibUnicode @trflynn89
|
/Libraries/LibUnicode @trflynn89
|
||||||
/Userland/Libraries/LibWasm @alimpfard
|
/Libraries/LibWasm @alimpfard
|
||||||
/Userland/Libraries/LibWeb/CSS @AtkinsSJ
|
/Libraries/LibWeb/CSS @AtkinsSJ
|
||||||
/Userland/Libraries/LibWeb/WebAssembly @alimpfard
|
/Libraries/LibWeb/WebAssembly @alimpfard
|
||||||
/Userland/Libraries/LibWeb/WebDriver @trflynn89
|
/Libraries/LibWeb/WebDriver @trflynn89
|
||||||
/Userland/Libraries/LibXML @alimpfard
|
/Libraries/LibXML @alimpfard
|
||||||
/Userland/Services/RequestServer @alimpfard
|
/Services/RequestServer @alimpfard
|
||||||
/Userland/Services/WebDriver @trflynn89
|
/Services/WebDriver @trflynn89
|
||||||
/Userland/Utilities/wasm.cpp @alimpfard
|
/Utilities/wasm.cpp @alimpfard
|
||||||
|
|
2
.github/workflows/lagom-template.yml
vendored
2
.github/workflows/lagom-template.yml
vendored
|
@ -191,7 +191,7 @@ jobs:
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: libweb-test-artifacts-${{ inputs.os_name }}
|
name: libweb-test-artifacts-${{ inputs.os_name }}
|
||||||
path: ${{ github.workspace }}/Build/Ladybird/test-dumps
|
path: ${{ github.workspace }}/Build/UI/test-dumps
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
|
4
.github/workflows/libjs-test262.yml
vendored
4
.github/workflows/libjs-test262.yml
vendored
|
@ -115,7 +115,7 @@ jobs:
|
||||||
- name: Run test-wasm
|
- name: Run test-wasm
|
||||||
working-directory: libjs-test262
|
working-directory: libjs-test262
|
||||||
run: |
|
run: |
|
||||||
Build/bin/test-wasm --per-file Build/Lagom/Userland/Libraries/LibWasm/Tests > ../libjs-data/wasm/per-file-master.json || true
|
Build/bin/test-wasm --per-file Build/Lagom/Libraries/LibWasm/Tests > ../libjs-data/wasm/per-file-master.json || true
|
||||||
jq -nc -f /dev/stdin <<-EOF --slurpfile previous ../libjs-data/wasm/results.json --slurpfile details ../libjs-data/wasm/per-file-master.json > wasm-new-results.json
|
jq -nc -f /dev/stdin <<-EOF --slurpfile previous ../libjs-data/wasm/results.json --slurpfile details ../libjs-data/wasm/per-file-master.json > wasm-new-results.json
|
||||||
\$details[0] as \$details | \$previous[0] + [{
|
\$details[0] as \$details | \$previous[0] + [{
|
||||||
"commit_timestamp": $(git -C .. log -1 --format=%ct),
|
"commit_timestamp": $(git -C .. log -1 --format=%ct),
|
||||||
|
@ -146,7 +146,7 @@ jobs:
|
||||||
run: ./libjs-test262/per_file_result_diff.py -o old-libjs-data/wasm/per-file-master.json -n libjs-data/wasm/per-file-master.json
|
run: ./libjs-test262/per_file_result_diff.py -o old-libjs-data/wasm/per-file-master.json -n libjs-data/wasm/per-file-master.json
|
||||||
|
|
||||||
- name: Deploy to GitHub
|
- name: Deploy to GitHub
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.6.8
|
uses: JamesIves/github-pages-deploy-action@v4.6.9
|
||||||
with:
|
with:
|
||||||
git-config-name: LadybirdBot
|
git-config-name: LadybirdBot
|
||||||
git-config-email: ladybirdbot@ladybird.org
|
git-config-email: ladybirdbot@ladybird.org
|
||||||
|
|
2
.github/workflows/nightly-android.yml
vendored
2
.github/workflows/nightly-android.yml
vendored
|
@ -91,7 +91,7 @@ jobs:
|
||||||
# === BUILD ===
|
# === BUILD ===
|
||||||
|
|
||||||
- name: Build and Test
|
- name: Build and Test
|
||||||
working-directory: ${{ github.workspace }}/Ladybird/Android
|
working-directory: ${{ github.workspace }}/UI/Android
|
||||||
run: ./gradlew connectedAndroidTest
|
run: ./gradlew connectedAndroidTest
|
||||||
env:
|
env:
|
||||||
GRADLE_OPTS: '-Xmx3072m'
|
GRADLE_OPTS: '-Xmx3072m'
|
||||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -40,8 +40,8 @@ local.properties
|
||||||
# We can't build from cmd.exe anyway
|
# We can't build from cmd.exe anyway
|
||||||
gradlew.bat
|
gradlew.bat
|
||||||
|
|
||||||
Userland/Libraries/LibWasm/Tests/Fixtures/SpecTests
|
Libraries/LibWasm/Tests/Fixtures/SpecTests
|
||||||
Userland/Libraries/LibWasm/Tests/Spec
|
Libraries/LibWasm/Tests/Spec
|
||||||
|
|
||||||
Tests/LibWeb/WPT/wpt
|
Tests/LibWeb/WPT/wpt
|
||||||
Tests/LibWeb/WPT/metadata
|
Tests/LibWeb/WPT/metadata
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
Userland/Libraries/LibJS/Tests/invalid-lhs-in-assignment.js
|
Libraries/LibJS/Tests/invalid-lhs-in-assignment.js
|
||||||
Userland/Libraries/LibJS/Tests/unicode-identifier-escape.js
|
Libraries/LibJS/Tests/unicode-identifier-escape.js
|
||||||
Userland/Libraries/LibJS/Tests/modules/failing.mjs
|
Libraries/LibJS/Tests/modules/failing.mjs
|
||||||
|
|
||||||
# FIXME: Remove once prettier is updated to support using declarations.
|
# FIXME: Remove once prettier is updated to support using declarations.
|
||||||
Userland/Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js
|
Libraries/LibJS/Tests/builtins/DisposableStack/DisposableStack.prototype.@@dispose.js
|
||||||
Userland/Libraries/LibJS/Tests/modules/top-level-dispose.mjs
|
Libraries/LibJS/Tests/modules/top-level-dispose.mjs
|
||||||
Userland/Libraries/LibJS/Tests/using-declaration.js
|
Libraries/LibJS/Tests/using-declaration.js
|
||||||
Userland/Libraries/LibJS/Tests/using-for-loops.js
|
Libraries/LibJS/Tests/using-for-loops.js
|
||||||
|
|
||||||
Tests/LibWeb/Ref/input/wpt-import
|
Tests/LibWeb/Ref/input/wpt-import
|
||||||
Tests/LibWeb/Text/input/wpt-import
|
Tests/LibWeb/Text/input/wpt-import
|
||||||
|
|
|
@ -23,10 +23,25 @@ extension Swift.String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AK.String {
|
||||||
|
public init(swiftString: consuming Swift.String) {
|
||||||
|
self.init() // Create empty string first, using default constructor
|
||||||
|
swiftString.withUTF8 { buffer in
|
||||||
|
self = AK.String.from_utf8_without_validation(AK.ReadonlyBytes(buffer.baseAddress!, buffer.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
extension AK.StringView: ExpressibleByStringLiteral {
|
extension AK.StringView: ExpressibleByStringLiteral {
|
||||||
public typealias StringLiteralType = Swift.StaticString
|
public typealias StringLiteralType = Swift.StaticString
|
||||||
|
|
||||||
public init(stringLiteral value: StringLiteralType) {
|
public init(stringLiteral value: StringLiteralType) {
|
||||||
self.init(value.utf8Start, value.utf8CodeUnitCount)
|
self.init(value.utf8Start, value.utf8CodeUnitCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func endsWith(_ suffix: AK.StringView) -> Bool {
|
||||||
|
if suffix.length() == 1 {
|
||||||
|
return self.ends_with(suffix[0])
|
||||||
|
}
|
||||||
|
return self.ends_with(suffix, AK.CaseSensitivity.sensitive)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,8 @@
|
||||||
#include <AK/BitmapView.h>
|
#include <AK/BitmapView.h>
|
||||||
#include <AK/Error.h>
|
#include <AK/Error.h>
|
||||||
#include <AK/Noncopyable.h>
|
#include <AK/Noncopyable.h>
|
||||||
#include <AK/Optional.h>
|
|
||||||
#include <AK/Platform.h>
|
#include <AK/Platform.h>
|
||||||
#include <AK/StdLibExtras.h>
|
#include <AK/StdLibExtras.h>
|
||||||
#include <AK/Try.h>
|
|
||||||
#include <AK/Types.h>
|
#include <AK/Types.h>
|
||||||
#include <AK/kmalloc.h>
|
#include <AK/kmalloc.h>
|
||||||
|
|
||||||
|
|
|
@ -387,9 +387,9 @@ Vector<size_t> ByteString::find_all(StringView needle) const
|
||||||
return StringUtils::find_all(*this, needle);
|
return StringUtils::find_all(*this, needle);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeprecatedStringCodePointIterator ByteString::code_points() const
|
Utf8CodePointIterator ByteString::code_points() const&
|
||||||
{
|
{
|
||||||
return DeprecatedStringCodePointIterator(*this);
|
return Utf8CodePointIterator { reinterpret_cast<u8 const*>(characters()), length() };
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorOr<ByteString> ByteString::from_utf8(ReadonlyBytes bytes)
|
ErrorOr<ByteString> ByteString::from_utf8(ReadonlyBytes bytes)
|
||||||
|
|
|
@ -141,7 +141,8 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] bool is_whitespace() const { return StringUtils::is_whitespace(*this); }
|
[[nodiscard]] bool is_whitespace() const { return StringUtils::is_whitespace(*this); }
|
||||||
|
|
||||||
[[nodiscard]] DeprecatedStringCodePointIterator code_points() const;
|
[[nodiscard]] Utf8CodePointIterator code_points() const&;
|
||||||
|
[[nodiscard]] Utf8CodePointIterator code_points() const&& = delete;
|
||||||
|
|
||||||
[[nodiscard]] ByteString trim(StringView characters, TrimMode mode = TrimMode::Both) const
|
[[nodiscard]] ByteString trim(StringView characters, TrimMode mode = TrimMode::Both) const
|
||||||
{
|
{
|
||||||
|
|
|
@ -50,6 +50,10 @@
|
||||||
# cmakedefine01 CSS_TRANSITIONS_DEBUG
|
# cmakedefine01 CSS_TRANSITIONS_DEBUG
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef DNS_DEBUG
|
||||||
|
# cmakedefine01 DNS_DEBUG
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef EDITOR_DEBUG
|
#ifndef EDITOR_DEBUG
|
||||||
# cmakedefine01 EDITOR_DEBUG
|
# cmakedefine01 EDITOR_DEBUG
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1012,6 +1012,13 @@ ErrorOr<void> Formatter<long double>::format(FormatBuilder& builder, long double
|
||||||
return builder.put_f80(value, base, upper_case, m_use_separator, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode);
|
return builder.put_f80(value, base, upper_case, m_use_separator, m_align, m_width.value(), m_precision.value(), m_fill, m_sign_mode, real_number_display_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorOr<void> Formatter<f16>::format(FormatBuilder& builder, f16 value)
|
||||||
|
{
|
||||||
|
// FIXME: Create a proper put_f16() implementation
|
||||||
|
Formatter<double> formatter { *this };
|
||||||
|
return TRY(formatter.format(builder, static_cast<double>(value)));
|
||||||
|
}
|
||||||
|
|
||||||
ErrorOr<void> Formatter<double>::format(FormatBuilder& builder, double value)
|
ErrorOr<void> Formatter<double>::format(FormatBuilder& builder, double value)
|
||||||
{
|
{
|
||||||
u8 base;
|
u8 base;
|
||||||
|
|
11
AK/Format.h
11
AK/Format.h
|
@ -551,6 +551,17 @@ struct Formatter<long double> : StandardFormatter {
|
||||||
ErrorOr<void> format(FormatBuilder&, long double value);
|
ErrorOr<void> format(FormatBuilder&, long double value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct Formatter<f16> : StandardFormatter {
|
||||||
|
Formatter() = default;
|
||||||
|
explicit Formatter(StandardFormatter formatter)
|
||||||
|
: StandardFormatter(formatter)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorOr<void> format(FormatBuilder&, f16 value);
|
||||||
|
};
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
struct Formatter<nullptr_t> : Formatter<FlatPtr> {
|
struct Formatter<nullptr_t> : Formatter<FlatPtr> {
|
||||||
ErrorOr<void> format(FormatBuilder& builder, nullptr_t)
|
ErrorOr<void> format(FormatBuilder& builder, nullptr_t)
|
||||||
|
|
|
@ -31,7 +31,6 @@ class ConstrainedStream;
|
||||||
class CountingStream;
|
class CountingStream;
|
||||||
class DeprecatedFlyString;
|
class DeprecatedFlyString;
|
||||||
class ByteString;
|
class ByteString;
|
||||||
class DeprecatedStringCodePointIterator;
|
|
||||||
class Duration;
|
class Duration;
|
||||||
class Error;
|
class Error;
|
||||||
class FlyString;
|
class FlyString;
|
||||||
|
@ -163,7 +162,6 @@ using AK::CircularQueue;
|
||||||
using AK::ConstrainedStream;
|
using AK::ConstrainedStream;
|
||||||
using AK::CountingStream;
|
using AK::CountingStream;
|
||||||
using AK::DeprecatedFlyString;
|
using AK::DeprecatedFlyString;
|
||||||
using AK::DeprecatedStringCodePointIterator;
|
|
||||||
using AK::DoublyLinkedList;
|
using AK::DoublyLinkedList;
|
||||||
using AK::Error;
|
using AK::Error;
|
||||||
using AK::ErrorOr;
|
using AK::ErrorOr;
|
||||||
|
|
|
@ -29,6 +29,15 @@ public:
|
||||||
m_data[i] = data[i];
|
m_data[i] = data[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr IPv6Address(Array<u8, 16> const& data)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < 16; i++)
|
||||||
|
m_data[i] = data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<SameAs<char const*> T>
|
||||||
|
constexpr IPv6Address(T const&) = delete; // Disable implicit conversion of char const* -> ipv4 -> ipv6
|
||||||
|
|
||||||
constexpr IPv6Address(IPv4Address const& ipv4_address)
|
constexpr IPv6Address(IPv4Address const& ipv4_address)
|
||||||
{
|
{
|
||||||
// IPv4 mapped IPv6 address
|
// IPv4 mapped IPv6 address
|
||||||
|
|
|
@ -58,9 +58,14 @@ LexicalPath::LexicalPath(ByteString path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LexicalPath::is_absolute() const
|
bool LexicalPath::is_absolute_path(StringView path)
|
||||||
{
|
{
|
||||||
return m_string.starts_with('/');
|
return path.starts_with('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LexicalPath::is_root() const
|
||||||
|
{
|
||||||
|
return m_string == "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<ByteString> LexicalPath::parts() const
|
Vector<ByteString> LexicalPath::parts() const
|
||||||
|
|
|
@ -26,7 +26,10 @@ public:
|
||||||
|
|
||||||
explicit LexicalPath(ByteString);
|
explicit LexicalPath(ByteString);
|
||||||
|
|
||||||
bool is_absolute() const;
|
static bool is_absolute_path(StringView path);
|
||||||
|
bool is_absolute() const { return is_absolute_path(m_string); }
|
||||||
|
bool is_root() const;
|
||||||
|
|
||||||
ByteString const& string() const { return m_string; }
|
ByteString const& string() const { return m_string; }
|
||||||
|
|
||||||
StringView dirname() const { return m_dirname; }
|
StringView dirname() const { return m_dirname; }
|
||||||
|
|
|
@ -10,14 +10,9 @@
|
||||||
|
|
||||||
namespace AK {
|
namespace AK {
|
||||||
|
|
||||||
static bool is_absolute_path(StringView path)
|
|
||||||
{
|
|
||||||
return path.length() >= 2 && path[1] == ':';
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool is_root(auto const& parts)
|
static bool is_root(auto const& parts)
|
||||||
{
|
{
|
||||||
return parts.size() == 1 && is_absolute_path(parts[0]);
|
return parts.size() == 1 && LexicalPath::is_absolute_path(parts[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
LexicalPath::LexicalPath(ByteString path)
|
LexicalPath::LexicalPath(ByteString path)
|
||||||
|
@ -45,9 +40,14 @@ LexicalPath::LexicalPath(ByteString path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LexicalPath::is_absolute() const
|
bool LexicalPath::is_absolute_path(StringView path)
|
||||||
{
|
{
|
||||||
return is_absolute_path(m_string);
|
return path.length() >= 2 && path[1] == ':';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LexicalPath::is_root() const
|
||||||
|
{
|
||||||
|
return AK::is_root(m_parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<ByteString> LexicalPath::parts() const
|
Vector<ByteString> LexicalPath::parts() const
|
||||||
|
@ -86,7 +86,7 @@ ByteString LexicalPath::canonicalized_path(ByteString path)
|
||||||
continue;
|
continue;
|
||||||
if (part == ".." && !canonical_parts.is_empty()) {
|
if (part == ".." && !canonical_parts.is_empty()) {
|
||||||
// At the root, .. does nothing.
|
// At the root, .. does nothing.
|
||||||
if (is_root(canonical_parts))
|
if (AK::is_root(canonical_parts))
|
||||||
continue;
|
continue;
|
||||||
// A .. and a previous non-.. part cancel each other.
|
// A .. and a previous non-.. part cancel each other.
|
||||||
if (canonical_parts.last() != "..") {
|
if (canonical_parts.last() != "..") {
|
||||||
|
@ -100,7 +100,7 @@ ByteString LexicalPath::canonicalized_path(ByteString path)
|
||||||
StringBuilder builder;
|
StringBuilder builder;
|
||||||
builder.join('\\', canonical_parts);
|
builder.join('\\', canonical_parts);
|
||||||
// "X:" -> "X:\"
|
// "X:" -> "X:\"
|
||||||
if (is_root(canonical_parts))
|
if (AK::is_root(canonical_parts))
|
||||||
builder.append('\\');
|
builder.append('\\');
|
||||||
path = builder.to_byte_string();
|
path = builder.to_byte_string();
|
||||||
return path == "" ? "." : path;
|
return path == "" ? "." : path;
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <AK/Noncopyable.h>
|
#include <AK/Noncopyable.h>
|
||||||
|
#include <AK/StdLibExtras.h>
|
||||||
#include <AK/Types.h>
|
#include <AK/Types.h>
|
||||||
|
|
||||||
namespace AK {
|
namespace AK {
|
||||||
|
|
|
@ -73,16 +73,20 @@ String human_readable_size_long(u64 size, UseThousandsSeparator use_thousands_se
|
||||||
return MUST(String::formatted("{} ({} bytes)", human_readable_size_string, size));
|
return MUST(String::formatted("{} ({} bytes)", human_readable_size_string, size));
|
||||||
}
|
}
|
||||||
|
|
||||||
String human_readable_time(i64 time_in_seconds)
|
String human_readable_time(Duration duration)
|
||||||
{
|
{
|
||||||
auto days = time_in_seconds / 86400;
|
auto milliseconds = duration.to_milliseconds();
|
||||||
time_in_seconds = time_in_seconds % 86400;
|
|
||||||
|
|
||||||
auto hours = time_in_seconds / 3600;
|
auto days = milliseconds / 86400000;
|
||||||
time_in_seconds = time_in_seconds % 3600;
|
milliseconds = milliseconds % 86400000;
|
||||||
|
|
||||||
auto minutes = time_in_seconds / 60;
|
auto hours = milliseconds / 3600000;
|
||||||
time_in_seconds = time_in_seconds % 60;
|
milliseconds = milliseconds % 3600000;
|
||||||
|
|
||||||
|
auto minutes = milliseconds / 60000;
|
||||||
|
milliseconds = milliseconds % 60000;
|
||||||
|
|
||||||
|
auto seconds = static_cast<double>(milliseconds) / 1000.0;
|
||||||
|
|
||||||
StringBuilder builder;
|
StringBuilder builder;
|
||||||
|
|
||||||
|
@ -95,7 +99,7 @@ String human_readable_time(i64 time_in_seconds)
|
||||||
if (minutes > 0)
|
if (minutes > 0)
|
||||||
builder.appendff("{} minute{} ", minutes, minutes == 1 ? "" : "s");
|
builder.appendff("{} minute{} ", minutes, minutes == 1 ? "" : "s");
|
||||||
|
|
||||||
builder.appendff("{} second{}", time_in_seconds, time_in_seconds == 1 ? "" : "s");
|
builder.appendff("{:.3} second{}", seconds, seconds == 1.0 ? "" : "s");
|
||||||
|
|
||||||
return MUST(builder.to_string());
|
return MUST(builder.to_string());
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <AK/String.h>
|
#include <AK/String.h>
|
||||||
|
#include <AK/Time.h>
|
||||||
|
|
||||||
namespace AK {
|
namespace AK {
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ String human_readable_size(u64 size, HumanReadableBasedOn based_on = HumanReadab
|
||||||
String human_readable_quantity(u64 quantity, HumanReadableBasedOn based_on = HumanReadableBasedOn::Base2, StringView unit = "B"sv, UseThousandsSeparator use_thousands_separator = UseThousandsSeparator::No);
|
String human_readable_quantity(u64 quantity, HumanReadableBasedOn based_on = HumanReadableBasedOn::Base2, StringView unit = "B"sv, UseThousandsSeparator use_thousands_separator = UseThousandsSeparator::No);
|
||||||
|
|
||||||
String human_readable_size_long(u64 size, UseThousandsSeparator use_thousands_separator = UseThousandsSeparator::No);
|
String human_readable_size_long(u64 size, UseThousandsSeparator use_thousands_separator = UseThousandsSeparator::No);
|
||||||
String human_readable_time(i64 time_in_seconds);
|
String human_readable_time(Duration);
|
||||||
String human_readable_digital_time(i64 time_in_seconds);
|
String human_readable_digital_time(i64 time_in_seconds);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,6 +143,17 @@ struct NumericLimits<long double> {
|
||||||
static constexpr size_t digits() { return __LDBL_MANT_DIG__; }
|
static constexpr size_t digits() { return __LDBL_MANT_DIG__; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template<>
|
||||||
|
struct NumericLimits<f16> {
|
||||||
|
static constexpr f16 lowest() { return -__FLT16_MAX__; }
|
||||||
|
static constexpr f16 min_normal() { return __FLT16_MIN__; }
|
||||||
|
static constexpr f16 min_denormal() { return __FLT16_DENORM_MIN__; }
|
||||||
|
static constexpr f16 max() { return __FLT16_MAX__; }
|
||||||
|
static constexpr f16 epsilon() { return __FLT16_EPSILON__; }
|
||||||
|
static constexpr bool is_signed() { return true; }
|
||||||
|
static constexpr size_t digits() { return __FLT16_MANT_DIG__; }
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if USING_AK_GLOBALLY
|
#if USING_AK_GLOBALLY
|
||||||
|
|
|
@ -65,11 +65,16 @@ StackInfo::StackInfo()
|
||||||
// MacOS seems inconsistent on what stack size is given for the main thread.
|
// MacOS seems inconsistent on what stack size is given for the main thread.
|
||||||
// According to the Apple docs, default for main thread is 8MB, and default for
|
// According to the Apple docs, default for main thread is 8MB, and default for
|
||||||
// other threads is 512KB
|
// other threads is 512KB
|
||||||
constexpr size_t eight_megabytes = 0x800000;
|
if (pthread_main_np() == 1) {
|
||||||
if (pthread_main_np() == 1 && m_size < eight_megabytes) {
|
// Apparently the main thread's stack size is not reported correctly on macOS
|
||||||
// Assume no one messed with stack size linker options for the main thread,
|
// but we can use getrlimit to get the correct value.
|
||||||
// and just set it to 8MB.
|
rlimit limit {};
|
||||||
m_size = eight_megabytes;
|
getrlimit(RLIMIT_STACK, &limit);
|
||||||
|
if (limit.rlim_cur == RLIM_INFINITY) {
|
||||||
|
m_size = 8 * MiB;
|
||||||
|
} else {
|
||||||
|
m_size = limit.rlim_cur;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
m_base = top_of_stack - m_size;
|
m_base = top_of_stack - m_size;
|
||||||
#elif defined(AK_OS_OPENBSD)
|
#elif defined(AK_OS_OPENBSD)
|
||||||
|
|
|
@ -350,6 +350,8 @@ template<>
|
||||||
inline constexpr bool __IsFloatingPoint<double> = true;
|
inline constexpr bool __IsFloatingPoint<double> = true;
|
||||||
template<>
|
template<>
|
||||||
inline constexpr bool __IsFloatingPoint<long double> = true;
|
inline constexpr bool __IsFloatingPoint<long double> = true;
|
||||||
|
template<>
|
||||||
|
inline constexpr bool __IsFloatingPoint<f16> = true;
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
inline constexpr bool IsFloatingPoint = __IsFloatingPoint<RemoveCV<T>>;
|
inline constexpr bool IsFloatingPoint = __IsFloatingPoint<RemoveCV<T>>;
|
||||||
|
|
|
@ -283,6 +283,9 @@ public:
|
||||||
|
|
||||||
[[nodiscard]] constexpr int compare(StringView other) const
|
[[nodiscard]] constexpr int compare(StringView other) const
|
||||||
{
|
{
|
||||||
|
if (m_length == 0 && other.m_length == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
if (m_characters == nullptr)
|
if (m_characters == nullptr)
|
||||||
return other.m_characters ? -1 : 0;
|
return other.m_characters ? -1 : 0;
|
||||||
|
|
||||||
|
|
|
@ -18,4 +18,11 @@
|
||||||
# define SWIFT_CONFORMS_TO_PROTOCOL(protocol)
|
# define SWIFT_CONFORMS_TO_PROTOCOL(protocol)
|
||||||
# define SWIFT_COMPUTED_PROPERTY
|
# define SWIFT_COMPUTED_PROPERTY
|
||||||
# define SWIFT_MUTATING
|
# define SWIFT_MUTATING
|
||||||
|
# define SWIFT_UNCHECKED_SENDABLE
|
||||||
|
# define SWIFT_NONCOPYABLE
|
||||||
|
# define SWIFT_NONESCAPABLE
|
||||||
|
# define SWIFT_ESCAPABLE
|
||||||
|
# define SWIFT_ESCAPABLE_IF(...)
|
||||||
|
# define SWIFT_RETURNS_RETAINED
|
||||||
|
# define SWIFT_RETURNS_UNRETAINED
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -17,6 +17,9 @@ using i32 = __INT32_TYPE__;
|
||||||
using i16 = __INT16_TYPE__;
|
using i16 = __INT16_TYPE__;
|
||||||
using i8 = __INT8_TYPE__;
|
using i8 = __INT8_TYPE__;
|
||||||
|
|
||||||
|
using f16 = _Float16;
|
||||||
|
static_assert(__FLT16_MANT_DIG__ == 11 && __FLT16_MAX_EXP__ == 16);
|
||||||
|
|
||||||
using f32 = float;
|
using f32 = float;
|
||||||
static_assert(__FLT_MANT_DIG__ == 24 && __FLT_MAX_EXP__ == 128);
|
static_assert(__FLT_MANT_DIG__ == 24 && __FLT_MAX_EXP__ == 128);
|
||||||
|
|
||||||
|
@ -144,6 +147,7 @@ using __ptrdiff_t = __PTRDIFF_TYPE__;
|
||||||
# if defined(AK_OS_WINDOWS)
|
# if defined(AK_OS_WINDOWS)
|
||||||
using ssize_t = AK::Detail::MakeSigned<size_t>;
|
using ssize_t = AK::Detail::MakeSigned<size_t>;
|
||||||
using mode_t = unsigned short;
|
using mode_t = unsigned short;
|
||||||
|
using pid_t = int;
|
||||||
# endif
|
# endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <AK/ByteString.h>
|
#include <AK/ByteString.h>
|
||||||
#include <AK/Debug.h>
|
#include <AK/Debug.h>
|
||||||
#include <AK/Format.h>
|
#include <AK/Format.h>
|
||||||
|
#include <AK/Function.h>
|
||||||
#include <AK/StringView.h>
|
#include <AK/StringView.h>
|
||||||
#include <AK/Types.h>
|
#include <AK/Types.h>
|
||||||
|
|
||||||
|
@ -19,6 +20,7 @@ class Utf8View;
|
||||||
|
|
||||||
class Utf8CodePointIterator {
|
class Utf8CodePointIterator {
|
||||||
friend class Utf8View;
|
friend class Utf8View;
|
||||||
|
friend class ByteString;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Utf8CodePointIterator() = default;
|
Utf8CodePointIterator() = default;
|
||||||
|
@ -138,6 +140,52 @@ public:
|
||||||
|
|
||||||
bool validate(size_t& valid_bytes, AllowSurrogates allow_surrogates = AllowSurrogates::Yes) const;
|
bool validate(size_t& valid_bytes, AllowSurrogates allow_surrogates = AllowSurrogates::Yes) const;
|
||||||
|
|
||||||
|
template<typename Callback>
|
||||||
|
auto for_each_split_view(Function<bool(u32)> splitter, SplitBehavior split_behavior, Callback callback) const
|
||||||
|
{
|
||||||
|
bool keep_empty = has_flag(split_behavior, SplitBehavior::KeepEmpty);
|
||||||
|
bool keep_trailing_separator = has_flag(split_behavior, SplitBehavior::KeepTrailingSeparator);
|
||||||
|
|
||||||
|
auto start_offset = 0u;
|
||||||
|
auto offset = 0u;
|
||||||
|
|
||||||
|
auto run_callback = [&]() {
|
||||||
|
auto length = offset - start_offset;
|
||||||
|
|
||||||
|
if (length == 0 && !keep_empty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto substring = unicode_substring_view(start_offset, length);
|
||||||
|
|
||||||
|
// Reject splitter-only entries if we're not keeping empty results
|
||||||
|
if (keep_trailing_separator && !keep_empty && length == 1 && splitter(*substring.begin()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
callback(substring);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto iterator = begin();
|
||||||
|
while (iterator != end()) {
|
||||||
|
if (splitter(*iterator)) {
|
||||||
|
if (keep_trailing_separator)
|
||||||
|
++offset;
|
||||||
|
|
||||||
|
run_callback();
|
||||||
|
|
||||||
|
if (!keep_trailing_separator)
|
||||||
|
++offset;
|
||||||
|
|
||||||
|
start_offset = offset;
|
||||||
|
++iterator;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++offset;
|
||||||
|
++iterator;
|
||||||
|
}
|
||||||
|
run_callback();
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Utf8CodePointIterator;
|
friend class Utf8CodePointIterator;
|
||||||
|
|
||||||
|
@ -184,40 +232,6 @@ private:
|
||||||
mutable bool m_have_length { false };
|
mutable bool m_have_length { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeprecatedStringCodePointIterator {
|
|
||||||
public:
|
|
||||||
Optional<u32> next()
|
|
||||||
{
|
|
||||||
if (m_it.done())
|
|
||||||
return {};
|
|
||||||
auto value = *m_it;
|
|
||||||
++m_it;
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] Optional<u32> peek() const
|
|
||||||
{
|
|
||||||
if (m_it.done())
|
|
||||||
return {};
|
|
||||||
return *m_it;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] size_t byte_offset() const
|
|
||||||
{
|
|
||||||
return Utf8View(m_string).byte_offset_of(m_it);
|
|
||||||
}
|
|
||||||
|
|
||||||
DeprecatedStringCodePointIterator(ByteString string)
|
|
||||||
: m_string(move(string))
|
|
||||||
, m_it(Utf8View(m_string).begin())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
ByteString m_string;
|
|
||||||
Utf8CodePointIterator m_it;
|
|
||||||
};
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
struct Formatter<Utf8View> : Formatter<StringView> {
|
struct Formatter<Utf8View> : Formatter<StringView> {
|
||||||
ErrorOr<void> format(FormatBuilder&, Utf8View const&);
|
ErrorOr<void> format(FormatBuilder&, Utf8View const&);
|
||||||
|
@ -312,7 +326,6 @@ inline u32 Utf8CodePointIterator::operator*() const
|
||||||
}
|
}
|
||||||
|
|
||||||
#if USING_AK_GLOBALLY
|
#if USING_AK_GLOBALLY
|
||||||
using AK::DeprecatedStringCodePointIterator;
|
|
||||||
using AK::Utf8CodePointIterator;
|
using AK::Utf8CodePointIterator;
|
||||||
using AK::Utf8View;
|
using AK::Utf8View;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -4,7 +4,7 @@ if (VCPKG_TARGET_ANDROID)
|
||||||
# If we are building for Android, we must load vcpkg_android.cmake before the project() declaration.
|
# If we are building for Android, we must load vcpkg_android.cmake before the project() declaration.
|
||||||
# This ensures that the CMAKE_TOOLCHAIN_FILE is set correctly.
|
# This ensures that the CMAKE_TOOLCHAIN_FILE is set correctly.
|
||||||
# (we cannot set CMAKE_TOOLCHAIN_FILE from Gradle, unfortunately, so this is the only place we can do it.)
|
# (we cannot set CMAKE_TOOLCHAIN_FILE from Gradle, unfortunately, so this is the only place we can do it.)
|
||||||
include("Ladybird/Android/vcpkg_android.cmake")
|
include("UI/Android/vcpkg_android.cmake")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Pass additional information to vcpkg toolchain files if we are using vcpkg.
|
# Pass additional information to vcpkg toolchain files if we are using vcpkg.
|
||||||
|
@ -28,7 +28,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(LADYBIRD_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
set(LADYBIRD_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
list(APPEND CMAKE_MODULE_PATH "${LADYBIRD_SOURCE_DIR}/Meta/CMake")
|
list(APPEND CMAKE_MODULE_PATH "${LADYBIRD_SOURCE_DIR}/Meta/CMake")
|
||||||
|
|
||||||
include(Ladybird/cmake/EnableLagom.cmake)
|
include(UI/cmake/EnableLagom.cmake)
|
||||||
include(use_linker)
|
include(use_linker)
|
||||||
include(lagom_options NO_POLICY_SCOPE)
|
include(lagom_options NO_POLICY_SCOPE)
|
||||||
include(lagom_compile_options)
|
include(lagom_compile_options)
|
||||||
|
@ -68,12 +68,6 @@ endif()
|
||||||
add_cxx_compile_options(-Wno-expansion-to-defined)
|
add_cxx_compile_options(-Wno-expansion-to-defined)
|
||||||
add_cxx_compile_options(-Wno-user-defined-literals)
|
add_cxx_compile_options(-Wno-user-defined-literals)
|
||||||
|
|
||||||
if (ANDROID OR APPLE)
|
|
||||||
serenity_option(ENABLE_QT OFF CACHE BOOL "Build ladybird application using Qt GUI")
|
|
||||||
else()
|
|
||||||
serenity_option(ENABLE_QT ON CACHE BOOL "Build ladybird application using Qt GUI")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (ANDROID AND ENABLE_QT)
|
if (ANDROID AND ENABLE_QT)
|
||||||
message(STATUS "Disabling Qt for Android")
|
message(STATUS "Disabling Qt for Android")
|
||||||
set(ENABLE_QT OFF CACHE BOOL "" FORCE)
|
set(ENABLE_QT OFF CACHE BOOL "" FORCE)
|
||||||
|
@ -89,8 +83,8 @@ endif()
|
||||||
include(CTest) # for BUILD_TESTING option, default ON
|
include(CTest) # for BUILD_TESTING option, default ON
|
||||||
|
|
||||||
if (ENABLE_GUI_TARGETS)
|
if (ENABLE_GUI_TARGETS)
|
||||||
add_subdirectory(Userland/Services)
|
add_subdirectory(Services)
|
||||||
add_subdirectory(Ladybird)
|
add_subdirectory(UI)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_custom_target(lint-shell-scripts
|
add_custom_target(lint-shell-scripts
|
||||||
|
|
|
@ -73,7 +73,7 @@ Nobody is perfect, and sometimes we mess things up. That said, here are some goo
|
||||||
* Wrap your commit messages at 72 characters.
|
* Wrap your commit messages at 72 characters.
|
||||||
* The first line of the commit message is the subject line, and must have the format "Category: Brief description of what's being changed". The category should be the name of a library, application, service, utility, etc.
|
* The first line of the commit message is the subject line, and must have the format "Category: Brief description of what's being changed". The category should be the name of a library, application, service, utility, etc.
|
||||||
* Examples: `LibMedia`, `WebContent`, `CI`, `AK`, `RequestServer`, `js`
|
* Examples: `LibMedia`, `WebContent`, `CI`, `AK`, `RequestServer`, `js`
|
||||||
* Don't use a category like "`Userland`" or "`Utilities`", except for generic changes that affect a large portion of code within these directories.
|
* Don't use a category like "`Libraries`" or "`Utilities`", except for generic changes that affect a large portion of code within these directories.
|
||||||
* Don't use specific component names, e.g. C++ class names, as the category either - mention them in the summary instead. E.g. `LibGUI: Brief description of what's being changed in FooWidget` rather than `FooWidget: Brief description of what's being changed`
|
* Don't use specific component names, e.g. C++ class names, as the category either - mention them in the summary instead. E.g. `LibGUI: Brief description of what's being changed in FooWidget` rather than `FooWidget: Brief description of what's being changed`
|
||||||
* Several categories may be combined with `+`, e.g. `LibJS+LibWeb+Browser: ...`
|
* Several categories may be combined with `+`, e.g. `LibJS+LibWeb+Browser: ...`
|
||||||
* Write the commit message subject line in the imperative mood ("Foo: Change the way dates work", not "Foo: Changed the way dates work").
|
* Write the commit message subject line in the imperative mood ("Foo: Change the way dates work", not "Foo: Changed the way dates work").
|
||||||
|
|
|
@ -23,9 +23,9 @@ interface CSSRule {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Add a `libweb_js_bindings(HTML/HTMLDetailsElement)` call to [`LibWeb/idl_files.cmake`](../Userland/Libraries/LibWeb/idl_files.cmake)
|
3. Add a `libweb_js_bindings(HTML/HTMLDetailsElement)` call to [`LibWeb/idl_files.cmake`](../Libraries/LibWeb/idl_files.cmake)
|
||||||
|
|
||||||
4. Forward declare the generated class in [`LibWeb/Forward.h`](../Userland/Libraries/LibWeb/Forward.h):
|
4. Forward declare the generated class in [`LibWeb/Forward.h`](../Libraries/LibWeb/Forward.h):
|
||||||
- `HTMLDetailsElement` in its namespace.
|
- `HTMLDetailsElement` in its namespace.
|
||||||
|
|
||||||
5. If your type isn't an Event or Element, you will need to add it to [`is_platform_object()`](../Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp)
|
5. If your type isn't an Event or Element, you will need to add it to [`is_platform_object()`](../Meta/Lagom/Tools/CodeGenerators/LibWeb/BindingsGenerator/IDLGenerators.cpp)
|
||||||
|
|
|
@ -188,7 +188,7 @@ pkgman install cmake cmd:python3 ninja openal_devel qt6_base_devel qt6_multimedi
|
||||||
### Android:
|
### Android:
|
||||||
|
|
||||||
On a Unix-like platform, install the prerequisites for that platform and then see the [Android Studio guide](EditorConfiguration/AndroidStudioConfiguration.md).
|
On a Unix-like platform, install the prerequisites for that platform and then see the [Android Studio guide](EditorConfiguration/AndroidStudioConfiguration.md).
|
||||||
Or, download a version of Gradle >= 8.0.0, and run the ``gradlew`` program in ``Ladybird/Android``
|
Or, download a version of Gradle >= 8.0.0, and run the ``gradlew`` program in ``UI/Android``
|
||||||
|
|
||||||
## Build steps
|
## Build steps
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ The script Meta/ladybird.sh and the default preset in CMakePresets.json both def
|
||||||
`Build/release`. For distribution purposes, or when building multiple configurations, it may be useful to create a custom
|
`Build/release`. For distribution purposes, or when building multiple configurations, it may be useful to create a custom
|
||||||
CMake build directory.
|
CMake build directory.
|
||||||
|
|
||||||
The install rules in Ladybird/cmake/InstallRules.cmake define which binaries and libraries will be
|
The install rules in UI/cmake/InstallRules.cmake define which binaries and libraries will be
|
||||||
installed into the configured CMAKE_PREFIX_PATH or path passed to ``cmake --install``.
|
installed into the configured CMAKE_PREFIX_PATH or path passed to ``cmake --install``.
|
||||||
|
|
||||||
Note that when using a custom build directory rather than Meta/ladybird.sh, the user may need to provide
|
Note that when using a custom build directory rather than Meta/ladybird.sh, the user may need to provide
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# CSS Generated Files
|
# CSS Generated Files
|
||||||
|
|
||||||
We generate a significant amount of CSS-related code, taking in one or more .json files in
|
We generate a significant amount of CSS-related code, taking in one or more .json files in
|
||||||
[`Userland/Libraries/LibWeb/CSS`](../Userland/Libraries/LibWeb/CSS) and producing C++ code from them, located in
|
[`Libraries/LibWeb/CSS`](../Libraries/LibWeb/CSS) and producing C++ code from them, located in
|
||||||
`Build/<build-preset>/Lagom/Userland/Libraries/LibWeb/CSS/`.
|
`Build/<build-preset>/Lagom/Libraries/LibWeb/CSS/`.
|
||||||
It's likely that you'll need to work with these if you add or modify a CSS property or its values.
|
It's likely that you'll need to work with these if you add or modify a CSS property or its values.
|
||||||
|
|
||||||
The generators are found in [`Meta/Lagom/Tools/CodeGenerators/LibWeb`](../Meta/Lagom/Tools/CodeGenerators/LibWeb).
|
The generators are found in [`Meta/Lagom/Tools/CodeGenerators/LibWeb`](../Meta/Lagom/Tools/CodeGenerators/LibWeb).
|
||||||
|
@ -11,7 +11,7 @@ They are run automatically as part of the build, and most of the time you can ig
|
||||||
## Properties.json
|
## Properties.json
|
||||||
|
|
||||||
Each CSS property has an entry here, which describes what values it accepts, whether it's inherited, and similar data.
|
Each CSS property has an entry here, which describes what values it accepts, whether it's inherited, and similar data.
|
||||||
This generates `PropertyID.h` and `PropertyID.cpp`.
|
This generates `PropertyID.h`, `PropertyID.cpp`, `GeneratedCSSStyleProperties.h`, `GeneratedCSSStyleProperties.cpp` and `GeneratedCSSStyleProperties.idl`.
|
||||||
Most of this data is found in the information box for that property in the relevant CSS spec.
|
Most of this data is found in the information box for that property in the relevant CSS spec.
|
||||||
|
|
||||||
The file is organized as a single JSON object, with keys being property names, and the values being the data for that property.
|
The file is organized as a single JSON object, with keys being property names, and the values being the data for that property.
|
||||||
|
@ -173,7 +173,7 @@ Parameter definitions have the following properties:
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
|------------|----------------------------------------------|
|
|------------|----------------------------------------------|
|
||||||
| `type` | String. Accepted type for the parameter. |
|
| `type` | String. Accepted type for the parameter. |
|
||||||
| `required` | Boolean. Whether this parameter is required. |
|
| `required` | Boolean. Whether this parameter is required. |
|
||||||
|
|
||||||
The generated code provides:
|
The generated code provides:
|
||||||
|
|
|
@ -12,11 +12,11 @@ Ensure that your system has the following tools available:
|
||||||
|
|
||||||
## Opening the project
|
## Opening the project
|
||||||
|
|
||||||
After opening the ``ladybird`` directory in Android Studio (NOT the Ladybird/Android directory!)
|
After opening the ``ladybird`` directory in Android Studio (NOT the UI/Android directory!)
|
||||||
there should be a pop-up in the bottom left indicating that an Android Gradle project was detected
|
there should be a pop-up in the bottom left indicating that an Android Gradle project was detected
|
||||||
in ``Ladybird/Android``.
|
in ``UI/Android``.
|
||||||
|
|
||||||
In the top left of the screen in the Project view, navigate to ``Ladybird/Android``. Or, click the
|
In the top left of the screen in the Project view, navigate to ``UI/Android``. Or, click the
|
||||||
highlighted text in the notification for that path. Open the ``settings.gradle.kts`` file. At the
|
highlighted text in the notification for that path. Open the ``settings.gradle.kts`` file. At the
|
||||||
top of the file should be a banner that says ``Code Insight unavailable (related Gradle project not
|
top of the file should be a banner that says ``Code Insight unavailable (related Gradle project not
|
||||||
linked).`` Click the ``Link Gradle project`` text on the right side of the banner. After the IDE
|
linked).`` Click the ``Link Gradle project`` text on the right side of the banner. After the IDE
|
||||||
|
|
|
@ -18,9 +18,9 @@ these files navigate to the `Project` tool window, right-click the `Build` folde
|
||||||
|
|
||||||
## Include headers and source files for code insight
|
## Include headers and source files for code insight
|
||||||
|
|
||||||
To get proper code insight mark the folders `AK` and `Userland` by right-clicking on them and selecting `Mark Directory as | Project Sources and Headers`.
|
To get proper code insight mark the folders `AK` and `Libraries` by right-clicking on them and selecting `Mark Directory as | Project Sources and Headers`.
|
||||||
|
|
||||||
A symptom of this not being configured correctly is CLion giving a warning for every single file:
|
A symptom of this not being configured correctly is CLion giving a warning for every single file:
|
||||||
> The file does not belong to any project target, code insight features might not work properly.
|
> The file does not belong to any project target, code insight features might not work properly.
|
||||||
|
|
||||||
## Code Generation Settings
|
## Code Generation Settings
|
||||||
|
@ -46,7 +46,7 @@ CMake could not locate one.
|
||||||
|
|
||||||
This error typically arises when CLion is not configured to use the correct build directory.
|
This error typically arises when CLion is not configured to use the correct build directory.
|
||||||
|
|
||||||
**Solution**: Ensure that CLion's build directory is set to the correct build directory for the selected profile.
|
**Solution**: Ensure that CLion's build directory is set to the correct build directory for the selected profile.
|
||||||
Navigate to `Settings -> Build, Execution, Deployment -> CMake` and in your selected profile, set the `Build directory` according to the profile:
|
Navigate to `Settings -> Build, Execution, Deployment -> CMake` and in your selected profile, set the `Build directory` according to the profile:
|
||||||
- Default -> "`Build/ladybird`"
|
- Default -> "`Build/ladybird`"
|
||||||
- Debug -> "`Build/ladybird-debug`"
|
- Debug -> "`Build/ladybird-debug`"
|
||||||
|
|
|
@ -22,13 +22,11 @@ First, make sure you have a working toolchain and can build and run Ladybird. Go
|
||||||
* Edit the `ladybird.includes` file to list the following lines:
|
* Edit the `ladybird.includes` file to list the following lines:
|
||||||
```
|
```
|
||||||
./
|
./
|
||||||
Userland/
|
Libraries/
|
||||||
Userland/Libraries/
|
Services/
|
||||||
Userland/Services/
|
|
||||||
Build/release/
|
Build/release/
|
||||||
Build/release/Userland/
|
Build/release/Libraries/
|
||||||
Build/release/Userland/Libraries/
|
Build/release/Services/
|
||||||
Build/release/Userland/Services/
|
|
||||||
AK/
|
AK/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -59,12 +59,10 @@ following ``c_cpp_properties.json`` to circumvent some errors. Even with the con
|
||||||
"includePath": [
|
"includePath": [
|
||||||
"${workspaceFolder}",
|
"${workspaceFolder}",
|
||||||
"${workspaceFolder}/Build/release/",
|
"${workspaceFolder}/Build/release/",
|
||||||
"${workspaceFolder}/Build/release/Userland",
|
"${workspaceFolder}/Build/release/Libraries",
|
||||||
"${workspaceFolder}/Build/release/Userland/Libraries",
|
"${workspaceFolder}/Build/release/Services",
|
||||||
"${workspaceFolder}/Build/release/Userland/Services",
|
"${workspaceFolder}/Libraries",
|
||||||
"${workspaceFolder}/Userland",
|
"${workspaceFolder}/Services"
|
||||||
"${workspaceFolder}/Userland/Libraries",
|
|
||||||
"${workspaceFolder}/Userland/Services"
|
|
||||||
],
|
],
|
||||||
"defines": [
|
"defines": [
|
||||||
"DEBUG"
|
"DEBUG"
|
||||||
|
@ -82,12 +80,10 @@ following ``c_cpp_properties.json`` to circumvent some errors. Even with the con
|
||||||
"path": [
|
"path": [
|
||||||
"${workspaceFolder}",
|
"${workspaceFolder}",
|
||||||
"${workspaceFolder}/Build/release/",
|
"${workspaceFolder}/Build/release/",
|
||||||
"${workspaceFolder}/Build/release/Userland",
|
"${workspaceFolder}/Build/release/Libraries",
|
||||||
"${workspaceFolder}/Build/release/Userland/Libraries",
|
"${workspaceFolder}/Build/release/Services",
|
||||||
"${workspaceFolder}/Build/release/Userland/Services",
|
"${workspaceFolder}/Libraries",
|
||||||
"${workspaceFolder}/Userland",
|
"${workspaceFolder}/Services"
|
||||||
"${workspaceFolder}/Userland/Libraries",
|
|
||||||
"${workspaceFolder}/Userland/Services"
|
|
||||||
],
|
],
|
||||||
"limitSymbolsToIncludedHeaders": true,
|
"limitSymbolsToIncludedHeaders": true,
|
||||||
"databaseFilename": "${workspaceFolder}/Build/release/"
|
"databaseFilename": "${workspaceFolder}/Build/release/"
|
||||||
|
@ -290,7 +286,7 @@ The following three example tasks should suffice in most situations, and allow y
|
||||||
#### Mac
|
#### Mac
|
||||||
If you want to run the debugger, first place the content below in `.vscode/launch.json` in the root of the project.
|
If you want to run the debugger, first place the content below in `.vscode/launch.json` in the root of the project.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
|
@ -310,7 +306,7 @@ then run Ladybird with the debug preset and with the `--debug-process WebContent
|
||||||
CC=$(brew --prefix llvm)/bin/clang CXX=$(brew --prefix llvm)/bin/clang++ BUILD_PRESET=Debug ./Meta/ladybird.sh run ladybird --debug-process WebContent
|
CC=$(brew --prefix llvm)/bin/clang CXX=$(brew --prefix llvm)/bin/clang++ BUILD_PRESET=Debug ./Meta/ladybird.sh run ladybird --debug-process WebContent
|
||||||
```
|
```
|
||||||
|
|
||||||
Running Ladybird in this way will pause execution until a debugger is attached. You can then run the debugger by going to the **Run and Debug** menu and selecting the **Attach to WebContent** configuration.
|
Running Ladybird in this way will pause execution until a debugger is attached. You can then run the debugger by going to the **Run and Debug** menu and selecting the **Attach to WebContent** configuration.
|
||||||
|
|
||||||
#### Linux
|
#### Linux
|
||||||
For Linux, the `launch.json` will instead be the file below.
|
For Linux, the `launch.json` will instead be the file below.
|
||||||
|
|
|
@ -50,7 +50,7 @@ Here’s a short timeline:
|
||||||
|
|
||||||
> _I'd like to have rich text, and we might as well use HTML for that. :^)_
|
> _I'd like to have rich text, and we might as well use HTML for that. :^)_
|
||||||
|
|
||||||
LibHTML eventually became [LibWeb](https://github.com/LadybirdBrowser/ladybird/tree/master/Userland/Libraries/LibWeb) — which in turn eventually grew into being the core part of the browser engine and browser to which, on 4 July 2022, [the name _Ladybird_ was given](https://www.youtube.com/watch?v=X38MTKHt3_I&t=29s).
|
LibHTML eventually became [LibWeb](https://github.com/LadybirdBrowser/ladybird/tree/master/Libraries/LibWeb) — which in turn eventually grew into being the core part of the browser engine and browser to which, on 4 July 2022, [the name _Ladybird_ was given](https://www.youtube.com/watch?v=X38MTKHt3_I&t=29s).
|
||||||
|
|
||||||
- 2022 July: Renamed _Ladybird_ by Andreas in [“Let's make a Linux GUI for the SerenityOS browser”](https://youtu.be/X38MTKHt3_I) live-coding video.
|
- 2022 July: Renamed _Ladybird_ by Andreas in [“Let's make a Linux GUI for the SerenityOS browser”](https://youtu.be/X38MTKHt3_I) live-coding video.
|
||||||
- 2022 Sept: Spun off from SerenityOS to separate project: [“A new cross-platform browser project”](https://awesomekling.substack.com/p/ladybird-a-new-cross-platform-browser-project) announcement.
|
- 2022 Sept: Spun off from SerenityOS to separate project: [“A new cross-platform browser project”](https://awesomekling.substack.com/p/ladybird-a-new-cross-platform-browser-project) announcement.
|
||||||
|
|
|
@ -78,7 +78,7 @@ We separate CSS rules by their cascade origin. The two origins we're concerned w
|
||||||
|
|
||||||
The cascade origin determines the processing order for rules. The "user-agent" style is the least important, so it gets processed first. Then author style is added on top of that.
|
The cascade origin determines the processing order for rules. The "user-agent" style is the least important, so it gets processed first. Then author style is added on top of that.
|
||||||
|
|
||||||
Note: the user-agent style is a built-in CSS style sheet that lives in the LibWeb source code [here](https://github.com/LadybirdBrowser/ladybird/blob/master/Userland/Libraries/LibWeb/CSS/Default.css).
|
Note: the user-agent style is a built-in CSS style sheet that lives in the LibWeb source code [here](https://github.com/LadybirdBrowser/ladybird/blob/master/Libraries/LibWeb/CSS/Default.css).
|
||||||
|
|
||||||
The end product of style computation is a fully populated StyleProperties object. It has a CSSStyleValue for each CSS::PropertyID. In spec parlance, these are the *computed* values. (Note that these are not the same as you get from `getComputedStyle()`, that API returns the *resolved* values.)
|
The end product of style computation is a fully populated StyleProperties object. It has a CSSStyleValue for each CSS::PropertyID. In spec parlance, these are the *computed* values. (Note that these are not the same as you get from `getComputedStyle()`, that API returns the *resolved* values.)
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ This is the most common and at the same time most broad error type in LibWeb. In
|
||||||
variant of supported errors:
|
variant of supported errors:
|
||||||
|
|
||||||
- `SimpleException`
|
- `SimpleException`
|
||||||
- `JS::NonnullGCPtr<DOMException>`
|
- `GC::Ref<DOMException>`
|
||||||
- `JS::Completion` (from `JS::ThrowCompletionOr<T>`, assumed to be of `Type::Throw`)
|
- `JS::Completion` (from `JS::ThrowCompletionOr<T>`, assumed to be of `Type::Throw`)
|
||||||
|
|
||||||
Use this error type for anything that needs to interact with the JS bindings, which will generally
|
Use this error type for anything that needs to interact with the JS bindings, which will generally
|
||||||
|
@ -86,7 +86,7 @@ must have:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// https://fetch.spec.whatwg.org/#concept-fetch
|
// https://fetch.spec.whatwg.org/#concept-fetch
|
||||||
WebIDL::ExceptionOr<JS::NonnullGCPtr<Infrastructure::FetchController>> fetch(JS::Realm& realm, Infrastructure::Request& request, Infrastructure::FetchAlgorithms const& algorithms, UseParallelQueue use_parallel_queue)
|
WebIDL::ExceptionOr<GC::Ref<Infrastructure::FetchController>> fetch(JS::Realm& realm, Infrastructure::Request& request, Infrastructure::FetchAlgorithms const& algorithms, UseParallelQueue use_parallel_queue)
|
||||||
{
|
{
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ must have:
|
||||||
VERIFY(request.mode() == Infrastructure::Request::Mode::Navigate || !algorithms.process_early_hints_response().has_value());
|
VERIFY(request.mode() == Infrastructure::Request::Mode::Navigate || !algorithms.process_early_hints_response().has_value());
|
||||||
|
|
||||||
// 2. Let taskDestination be null.
|
// 2. Let taskDestination be null.
|
||||||
JS::GCPtr<JS::Object> task_destination;
|
GC::Ptr<JS::Object> task_destination;
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
```
|
```
|
||||||
|
|
|
@ -35,5 +35,5 @@ you are welcome to ask on [Discord](../README.md#get-in-touch-and-participate).
|
||||||
* [LibWeb: From Loading to Painting](LibWebFromLoadingToPainting.md)
|
* [LibWeb: From Loading to Painting](LibWebFromLoadingToPainting.md)
|
||||||
* [LibWeb: Browsing Contexts and Navigables](BrowsingContextsAndNavigables.md)
|
* [LibWeb: Browsing Contexts and Navigables](BrowsingContextsAndNavigables.md)
|
||||||
* [How to Add an IDL File](AddNewIDLFile.md)
|
* [How to Add an IDL File](AddNewIDLFile.md)
|
||||||
* [LibWeb Code Style & Patterns](Browser/Patterns.md)
|
* [LibWeb Code Style & Patterns](LibWebPatterns.md)
|
||||||
* [CSS Generated Files](CSSGeneratedFiles.md)
|
* [CSS Generated Files](CSSGeneratedFiles.md)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Testing Ladybird
|
# Testing Ladybird
|
||||||
|
|
||||||
Tests are locates in `Tests/`, with a directory for each library.
|
Tests are located in `Tests/`, with a directory for each library.
|
||||||
|
|
||||||
Every feature or bug fix added to LibWeb should have a corresponding test in `Tests/LibWeb`.
|
Every feature or bug fix added to LibWeb should have a corresponding test in `Tests/LibWeb`.
|
||||||
The test should be either a Text, Layout, Ref, or Screenshot test depending on the feature.
|
The test should be either a Text, Layout, Ref, or Screenshot test depending on the feature.
|
||||||
|
@ -12,9 +12,9 @@ Tests of internal C++ code go in their own `TestFoo.cpp` file in `Tests/LibWeb`.
|
||||||
> To reproduce a CI failure, see the section on [Running with Sanitizers](#running-with-sanitizers).
|
> To reproduce a CI failure, see the section on [Running with Sanitizers](#running-with-sanitizers).
|
||||||
|
|
||||||
The easiest way to run tests is to use the `ladybird.sh` script. The LibWeb tests are registered with CMake as a test in
|
The easiest way to run tests is to use the `ladybird.sh` script. The LibWeb tests are registered with CMake as a test in
|
||||||
`Ladybird/CMakeLists.txt`. Using the built-in test filtering, you can run all tests with `Meta/ladybird.sh test` or run
|
`UI/CMakeLists.txt`. Using the built-in test filtering, you can run all tests with `Meta/ladybird.sh test` or run
|
||||||
just the LibWeb tests with `Meta/ladybird.sh test LibWeb`. The second way is to invoke the headless browser test runner
|
just the LibWeb tests with `Meta/ladybird.sh test LibWeb`. The second way is to invoke the headless browser test runner
|
||||||
directly. See the invocation in `Ladybird/CMakeLists.txt` for the expected command line arguments.
|
directly. See the invocation in `UI/CMakeLists.txt` for the expected command line arguments.
|
||||||
|
|
||||||
A third way is to invoke `ctest` directly. The simplest method is to use the `default` preset from ``CMakePresets.json``:
|
A third way is to invoke `ctest` directly. The simplest method is to use the `default` preset from ``CMakePresets.json``:
|
||||||
|
|
||||||
|
|
|
@ -1,258 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <Ladybird/Utilities.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 <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 {};
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <Ladybird/Utilities.h>
|
|
||||||
#include <LibCore/ResourceImplementationFile.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);
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <Ladybird/Utilities.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>
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <Ladybird/FontPlugin.h>
|
|
||||||
#include <Ladybird/HelperProcess.h>
|
|
||||||
#include <Ladybird/ImageCodecPlugin.h>
|
|
||||||
#include <Ladybird/Utilities.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 <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 {};
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "WebViewImplementationNative.h"
|
|
||||||
#include "JNIHelpers.h"
|
|
||||||
#include <LibWebView/WebContentClient.h>
|
|
||||||
#include <Userland/Libraries/LibGfx/Bitmap.h>
|
|
||||||
#include <Userland/Libraries/LibGfx/DeprecatedPainter.h>
|
|
||||||
#include <Userland/Libraries/LibWeb/Crypto/Crypto.h>
|
|
||||||
#include <Userland/Libraries/LibWebView/ViewImplementation.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Userland/Libraries/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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
|
@ -1,156 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Ladybird/HelperProcess.h>
|
|
||||||
#include <Ladybird/Utilities.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/LadybirdWebViewBridge.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
|
|
|
@ -1,802 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <LibWebView/UserAgent.h>
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/Tab.h>
|
|
||||||
#import <UI/TabController.h>
|
|
||||||
|
|
||||||
#if defined(LADYBIRD_USE_SWIFT)
|
|
||||||
// FIXME: Report this codegen error to Apple
|
|
||||||
# define StyleMask NSWindowStyleMask
|
|
||||||
# import <Ladybird-Swift.h>
|
|
||||||
# undef StyleMask
|
|
||||||
#else
|
|
||||||
# import <UI/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
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,414 +0,0 @@
|
||||||
/*
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
add_library(ladybird_impl STATIC
|
|
||||||
${LADYBIRD_SOURCES}
|
|
||||||
Application/Application.mm
|
|
||||||
Application/ApplicationDelegate.mm
|
|
||||||
Application/EventLoopImplementation.mm
|
|
||||||
UI/Event.mm
|
|
||||||
UI/Inspector.mm
|
|
||||||
UI/InspectorController.mm
|
|
||||||
UI/LadybirdWebView.mm
|
|
||||||
UI/LadybirdWebViewBridge.cpp
|
|
||||||
UI/LadybirdWebViewWindow.mm
|
|
||||||
UI/Palette.mm
|
|
||||||
UI/SearchPanel.mm
|
|
||||||
UI/Tab.mm
|
|
||||||
UI/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
|
|
||||||
UI/TaskManager.swift
|
|
||||||
UI/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
|
|
||||||
UI/TaskManager.mm
|
|
||||||
UI/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)
|
|
|
@ -1,329 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <UI/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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <Ladybird/AppKit/UI/LadybirdWebViewWindow.h>
|
|
||||||
|
|
||||||
@class LadybirdWebView;
|
|
||||||
@class Tab;
|
|
||||||
|
|
||||||
@interface Inspector : LadybirdWebViewWindow
|
|
||||||
|
|
||||||
- (instancetype)init:(Tab*)tab;
|
|
||||||
|
|
||||||
- (void)inspect;
|
|
||||||
- (void)reset;
|
|
||||||
|
|
||||||
- (void)selectHoveredElement;
|
|
||||||
|
|
||||||
@end
|
|
|
@ -1,372 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <UI/Event.h>
|
|
||||||
#import <UI/Inspector.h>
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <UI/Inspector.h>
|
|
||||||
#import <UI/InspectorController.h>
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/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
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,198 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Ladybird/HelperProcess.h>
|
|
||||||
#include <Ladybird/Utilities.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/LadybirdWebViewBridge.h>
|
|
||||||
|
|
||||||
#import <UI/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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AK/ByteString.h>
|
|
||||||
#include <Ladybird/Utilities.h>
|
|
||||||
#include <LibCore/Resource.h>
|
|
||||||
#include <LibGfx/Palette.h>
|
|
||||||
#include <LibGfx/SystemTheme.h>
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <UI/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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,225 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <UI/LadybirdWebViewBridge.h>
|
|
||||||
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/SearchPanel.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <Ladybird/AppKit/UI/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
|
|
|
@ -1,396 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AK/ByteString.h>
|
|
||||||
#include <AK/String.h>
|
|
||||||
#include <Ladybird/Utilities.h>
|
|
||||||
#include <LibCore/Resource.h>
|
|
||||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
|
||||||
#include <LibGfx/ShareableBitmap.h>
|
|
||||||
#include <LibURL/URL.h>
|
|
||||||
#include <LibWebView/ViewImplementation.h>
|
|
||||||
|
|
||||||
#import <Application/ApplicationDelegate.h>
|
|
||||||
#import <UI/Inspector.h>
|
|
||||||
#import <UI/InspectorController.h>
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/SearchPanel.h>
|
|
||||||
#import <UI/Tab.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,725 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/Tab.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <Ladybird/AppKit/UI/LadybirdWebViewWindow.h>
|
|
||||||
|
|
||||||
@class LadybirdWebView;
|
|
||||||
|
|
||||||
@interface TaskManager : LadybirdWebViewWindow
|
|
||||||
|
|
||||||
- (instancetype)init;
|
|
||||||
|
|
||||||
@end
|
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#import <UI/LadybirdWebView.h>
|
|
||||||
#import <UI/TaskManager.h>
|
|
||||||
#import <UI/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
|
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AK/Enumerate.h>
|
|
||||||
#include <Ladybird/DefaultSettings.h>
|
|
||||||
#include <Ladybird/MachPortServer.h>
|
|
||||||
#include <Ladybird/Utilities.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>
|
|
||||||
|
|
||||||
#import <Application/Application.h>
|
|
||||||
#import <Application/ApplicationDelegate.h>
|
|
||||||
#import <Application/EventLoopImplementation.h>
|
|
||||||
#import <UI/Tab.h>
|
|
||||||
#import <UI/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();
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
module Ladybird [system] {
|
|
||||||
requires cplusplus
|
|
||||||
requires objc_arc
|
|
||||||
|
|
||||||
explicit module WebView {
|
|
||||||
header "UI/LadybirdWebView.h"
|
|
||||||
export *
|
|
||||||
}
|
|
||||||
|
|
||||||
explicit module WebViewWindow {
|
|
||||||
header "UI/LadybirdWebViewWindow.h"
|
|
||||||
export *
|
|
||||||
}
|
|
||||||
|
|
||||||
explicit module WebViewApplication {
|
|
||||||
header "../../Userland/Libraries/LibWebView/Application.h"
|
|
||||||
export *
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,149 +0,0 @@
|
||||||
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}/Ladybird/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}/Ladybird/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}/Userland/)
|
|
||||||
target_include_directories(${LADYBIRD_TARGET} ${LADYBIRD_SOURCE_DIR}/Userland/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()
|
|
|
@ -1,217 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022-2023, Andreas Kling <andreas@ladybird.org>
|
|
||||||
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "FontPlugin.h"
|
|
||||||
#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>
|
|
||||||
|
|
||||||
#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)];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/RefPtr.h>
|
|
||||||
#include <AK/Vector.h>
|
|
||||||
#include <LibGfx/Font/FontDatabase.h>
|
|
||||||
#include <LibWeb/Platform/FontPlugin.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class FontPlugin final : public Web::Platform::FontPlugin {
|
|
||||||
public:
|
|
||||||
FontPlugin(bool is_layout_test_mode, Gfx::SystemFontProvider* = nullptr);
|
|
||||||
virtual ~FontPlugin();
|
|
||||||
|
|
||||||
virtual Gfx::Font& default_font() override;
|
|
||||||
virtual Gfx::Font& default_fixed_width_font() override;
|
|
||||||
virtual RefPtr<Gfx::Font> default_emoji_font(float point_size) override;
|
|
||||||
virtual FlyString generic_font_name(Web::Platform::GenericFont) override;
|
|
||||||
|
|
||||||
void update_generic_fonts();
|
|
||||||
|
|
||||||
private:
|
|
||||||
Vector<FlyString> m_generic_font_names;
|
|
||||||
RefPtr<Gfx::Font> m_default_font;
|
|
||||||
RefPtr<Gfx::Font> m_default_fixed_width_font;
|
|
||||||
bool m_is_layout_test_mode { false };
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Ladybird/Headless/Application.h>
|
|
||||||
#include <Ladybird/Headless/Fixture.h>
|
|
||||||
#include <Ladybird/Headless/HeadlessWebView.h>
|
|
||||||
#include <Ladybird/HelperProcess.h>
|
|
||||||
#include <Ladybird/Utilities.h>
|
|
||||||
#include <LibCore/AnonymousBuffer.h>
|
|
||||||
#include <LibCore/ArgsParser.h>
|
|
||||||
#include <LibCore/System.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
Application::Application(Badge<WebView::Application>, Main::Arguments&)
|
|
||||||
: resources_folder(s_ladybird_resource_root)
|
|
||||||
, test_concurrency(Core::System::hardware_concurrency())
|
|
||||||
, python_executable_path("python3")
|
|
||||||
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
Application::~Application()
|
|
||||||
{
|
|
||||||
for (auto& fixture : Fixture::all())
|
|
||||||
fixture->teardown();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::create_platform_arguments(Core::ArgsParser& args_parser)
|
|
||||||
{
|
|
||||||
args_parser.add_option(screenshot_timeout, "Take a screenshot after [n] seconds (default: 1)", "screenshot", 's', "n");
|
|
||||||
args_parser.add_option(dump_layout_tree, "Dump layout tree and exit", "dump-layout-tree", 'd');
|
|
||||||
args_parser.add_option(dump_text, "Dump text and exit", "dump-text", 'T');
|
|
||||||
args_parser.add_option(test_concurrency, "Maximum number of tests to run at once", "test-concurrency", 'j', "jobs");
|
|
||||||
args_parser.add_option(python_executable_path, "Path to python3", "python-executable", 'P', "path");
|
|
||||||
args_parser.add_option(test_root_path, "Run tests in path", "run-tests", 'R', "test-root-path");
|
|
||||||
args_parser.add_option(test_glob, "Only run tests matching the given glob", "filter", 'f', "glob");
|
|
||||||
args_parser.add_option(test_dry_run, "List the tests that would be run, without running them", "dry-run");
|
|
||||||
args_parser.add_option(dump_failed_ref_tests, "Dump screenshots of failing ref tests", "dump-failed-ref-tests", 'D');
|
|
||||||
args_parser.add_option(dump_gc_graph, "Dump GC graph", "dump-gc-graph", 'G');
|
|
||||||
args_parser.add_option(resources_folder, "Path of the base resources folder (defaults to /res)", "resources", 'r', "resources-root-path");
|
|
||||||
args_parser.add_option(is_layout_test_mode, "Enable layout test mode", "layout-test-mode");
|
|
||||||
args_parser.add_option(rebaseline, "Rebaseline any executed layout or text tests", "rebaseline");
|
|
||||||
args_parser.add_option(log_slowest_tests, "Log the tests with the slowest run times", "log-slowest-tests");
|
|
||||||
args_parser.add_option(per_test_timeout_in_seconds, "Per-test timeout (default: 30)", "per-test-timeout", 't', "seconds");
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::create_platform_options(WebView::ChromeOptions& chrome_options, WebView::WebContentOptions& web_content_options)
|
|
||||||
{
|
|
||||||
if (!test_root_path.is_empty()) {
|
|
||||||
// --run-tests implies --layout-test-mode.
|
|
||||||
is_layout_test_mode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_layout_test_mode) {
|
|
||||||
// Allow window.open() to succeed for tests.
|
|
||||||
chrome_options.allow_popups = WebView::AllowPopups::Yes;
|
|
||||||
|
|
||||||
// Ensure consistent font rendering between operating systems.
|
|
||||||
web_content_options.force_fontconfig = WebView::ForceFontconfig::Yes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dump_gc_graph) {
|
|
||||||
// Force all tests to run in serial if we are interested in the GC graph.
|
|
||||||
test_concurrency = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
web_content_options.is_layout_test_mode = is_layout_test_mode ? WebView::IsLayoutTestMode::Yes : WebView::IsLayoutTestMode::No;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> Application::launch_services()
|
|
||||||
{
|
|
||||||
auto request_server_paths = TRY(get_paths_for_helper_process("RequestServer"sv));
|
|
||||||
m_request_client = TRY(launch_request_server_process(request_server_paths, resources_folder));
|
|
||||||
|
|
||||||
auto image_decoder_paths = TRY(get_paths_for_helper_process("ImageDecoder"sv));
|
|
||||||
m_image_decoder_client = TRY(launch_image_decoder_process(image_decoder_paths));
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> Application::launch_test_fixtures()
|
|
||||||
{
|
|
||||||
Fixture::initialize_fixtures();
|
|
||||||
|
|
||||||
// FIXME: Add option to only run specific fixtures from command line by name
|
|
||||||
// And an option to not run any fixtures at all
|
|
||||||
for (auto& fixture : Fixture::all()) {
|
|
||||||
if (auto result = fixture->setup(); result.is_error())
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
HeadlessWebView& Application::create_web_view(Core::AnonymousBuffer theme, Gfx::IntSize window_size)
|
|
||||||
{
|
|
||||||
auto web_view = HeadlessWebView::create(move(theme), window_size);
|
|
||||||
m_web_views.append(move(web_view));
|
|
||||||
|
|
||||||
return *m_web_views.last();
|
|
||||||
}
|
|
||||||
|
|
||||||
HeadlessWebView& Application::create_child_web_view(HeadlessWebView const& parent, u64 page_index)
|
|
||||||
{
|
|
||||||
auto web_view = HeadlessWebView::create_child(parent, page_index);
|
|
||||||
m_web_views.append(move(web_view));
|
|
||||||
|
|
||||||
return *m_web_views.last();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::destroy_web_views()
|
|
||||||
{
|
|
||||||
m_web_views.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/ByteString.h>
|
|
||||||
#include <AK/Error.h>
|
|
||||||
#include <AK/NonnullOwnPtr.h>
|
|
||||||
#include <AK/RefPtr.h>
|
|
||||||
#include <AK/Vector.h>
|
|
||||||
#include <LibGfx/Size.h>
|
|
||||||
#include <LibImageDecoderClient/Client.h>
|
|
||||||
#include <LibRequests/RequestClient.h>
|
|
||||||
#include <LibWebView/Application.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class HeadlessWebView;
|
|
||||||
|
|
||||||
class Application : public WebView::Application {
|
|
||||||
WEB_VIEW_APPLICATION(Application)
|
|
||||||
|
|
||||||
public:
|
|
||||||
~Application();
|
|
||||||
|
|
||||||
static Application& the()
|
|
||||||
{
|
|
||||||
return static_cast<Application&>(WebView::Application::the());
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void create_platform_arguments(Core::ArgsParser&) override;
|
|
||||||
virtual void create_platform_options(WebView::ChromeOptions&, WebView::WebContentOptions&) override;
|
|
||||||
|
|
||||||
ErrorOr<void> launch_services();
|
|
||||||
ErrorOr<void> launch_test_fixtures();
|
|
||||||
|
|
||||||
static Requests::RequestClient& request_client() { return *the().m_request_client; }
|
|
||||||
static ImageDecoderClient::Client& image_decoder_client() { return *the().m_image_decoder_client; }
|
|
||||||
|
|
||||||
HeadlessWebView& create_web_view(Core::AnonymousBuffer theme, Gfx::IntSize window_size);
|
|
||||||
HeadlessWebView& create_child_web_view(HeadlessWebView const&, u64 page_index);
|
|
||||||
void destroy_web_views();
|
|
||||||
|
|
||||||
template<typename Callback>
|
|
||||||
void for_each_web_view(Callback&& callback)
|
|
||||||
{
|
|
||||||
for (auto& web_view : m_web_views)
|
|
||||||
callback(*web_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
int screenshot_timeout { 1 };
|
|
||||||
ByteString resources_folder;
|
|
||||||
bool dump_failed_ref_tests { false };
|
|
||||||
bool dump_layout_tree { false };
|
|
||||||
bool dump_text { false };
|
|
||||||
bool dump_gc_graph { false };
|
|
||||||
bool is_layout_test_mode { false };
|
|
||||||
size_t test_concurrency { 1 };
|
|
||||||
ByteString python_executable_path;
|
|
||||||
ByteString test_root_path;
|
|
||||||
ByteString test_glob;
|
|
||||||
bool test_dry_run { false };
|
|
||||||
bool rebaseline { false };
|
|
||||||
bool log_slowest_tests { false };
|
|
||||||
int per_test_timeout_in_seconds { 30 };
|
|
||||||
|
|
||||||
private:
|
|
||||||
RefPtr<Requests::RequestClient> m_request_client;
|
|
||||||
RefPtr<ImageDecoderClient::Client> m_image_decoder_client;
|
|
||||||
|
|
||||||
Vector<NonnullOwnPtr<HeadlessWebView>> m_web_views;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
set(SOURCES
|
|
||||||
${LADYBIRD_SOURCES}
|
|
||||||
Application.cpp
|
|
||||||
Fixture.cpp
|
|
||||||
HeadlessWebView.cpp
|
|
||||||
Test.cpp
|
|
||||||
main.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(headless-browser ${SOURCES})
|
|
||||||
target_include_directories(headless-browser PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
|
||||||
target_include_directories(headless-browser PRIVATE ${LADYBIRD_SOURCE_DIR}/Userland/)
|
|
||||||
target_link_libraries(headless-browser PRIVATE ${LADYBIRD_LIBS} LibDiff)
|
|
||||||
|
|
||||||
if (BUILD_TESTING)
|
|
||||||
find_package(Python3 REQUIRED)
|
|
||||||
add_test(
|
|
||||||
NAME LibWeb
|
|
||||||
COMMAND $<TARGET_FILE:headless-browser> --run-tests ${LADYBIRD_SOURCE_DIR}/Tests/LibWeb --python-executable ${Python3_EXECUTABLE} --dump-failed-ref-tests --per-test-timeout 120
|
|
||||||
)
|
|
||||||
endif()
|
|
|
@ -1,96 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Andrew Kaster <andrew@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AK/LexicalPath.h>
|
|
||||||
#include <Ladybird/Headless/Application.h>
|
|
||||||
#include <Ladybird/Headless/Fixture.h>
|
|
||||||
#include <LibCore/Process.h>
|
|
||||||
#include <LibCore/StandardPaths.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
static ByteString s_fixtures_path;
|
|
||||||
|
|
||||||
// Key function for Fixture
|
|
||||||
Fixture::~Fixture() = default;
|
|
||||||
|
|
||||||
Optional<Fixture&> Fixture::lookup(StringView name)
|
|
||||||
{
|
|
||||||
for (auto& fixture : all()) {
|
|
||||||
if (fixture->name() == name)
|
|
||||||
return *fixture;
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector<NonnullOwnPtr<Fixture>>& Fixture::all()
|
|
||||||
{
|
|
||||||
static Vector<NonnullOwnPtr<Fixture>> fixtures;
|
|
||||||
return fixtures;
|
|
||||||
}
|
|
||||||
|
|
||||||
class HttpEchoServerFixture final : public Fixture {
|
|
||||||
public:
|
|
||||||
virtual ErrorOr<void> setup() override;
|
|
||||||
virtual void teardown_impl() override;
|
|
||||||
virtual StringView name() const override { return "HttpEchoServer"sv; }
|
|
||||||
virtual bool is_running() const override { return m_process.has_value(); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
ByteString m_script_path { "http-test-server.py" };
|
|
||||||
Optional<Core::Process> m_process;
|
|
||||||
};
|
|
||||||
|
|
||||||
ErrorOr<void> HttpEchoServerFixture::setup()
|
|
||||||
{
|
|
||||||
auto script_path = LexicalPath::join(s_fixtures_path, m_script_path);
|
|
||||||
|
|
||||||
// FIXME: Pick a more reasonable log path that is more observable
|
|
||||||
auto log_path = LexicalPath::join(Core::StandardPaths::tempfile_directory(), "http-test-server.log"sv).string();
|
|
||||||
|
|
||||||
auto arguments = Vector { script_path.string(), "start", "--directory", Ladybird::Application::the().test_root_path };
|
|
||||||
auto process_options = Core::ProcessSpawnOptions {
|
|
||||||
.executable = Ladybird::Application::the().python_executable_path,
|
|
||||||
.search_for_executable_in_path = true,
|
|
||||||
.arguments = arguments,
|
|
||||||
.file_actions = {
|
|
||||||
Core::FileAction::OpenFile { ByteString::formatted("{}.stdout", log_path), Core::File::OpenMode::Write, STDOUT_FILENO },
|
|
||||||
Core::FileAction::OpenFile { ByteString::formatted("{}.stderr", log_path), Core::File::OpenMode::Write, STDERR_FILENO },
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
m_process = TRY(Core::Process::spawn(process_options));
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void HttpEchoServerFixture::teardown_impl()
|
|
||||||
{
|
|
||||||
VERIFY(m_process.has_value());
|
|
||||||
|
|
||||||
auto script_path = LexicalPath::join(s_fixtures_path, m_script_path);
|
|
||||||
|
|
||||||
auto ret = Core::System::kill(m_process->pid(), SIGINT);
|
|
||||||
if (ret.is_error() && ret.error().code() != ESRCH) {
|
|
||||||
warnln("Failed to kill http-test-server.py: {}", ret.error());
|
|
||||||
m_process = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MUST(m_process->wait_for_termination());
|
|
||||||
|
|
||||||
m_process = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fixture::initialize_fixtures()
|
|
||||||
{
|
|
||||||
s_fixtures_path = LexicalPath::join(Ladybird::Application::the().test_root_path, "Fixtures"sv).string();
|
|
||||||
|
|
||||||
auto& registry = all();
|
|
||||||
registry.append(make<HttpEchoServerFixture>());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,205 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Ladybird/Headless/Application.h>
|
|
||||||
#include <Ladybird/Headless/HeadlessWebView.h>
|
|
||||||
#include <Ladybird/HelperProcess.h>
|
|
||||||
#include <Ladybird/Utilities.h>
|
|
||||||
#include <LibGfx/Bitmap.h>
|
|
||||||
#include <LibGfx/ShareableBitmap.h>
|
|
||||||
#include <LibWeb/Crypto/Crypto.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
static Web::DevicePixelRect const screen_rect { 0, 0, 1920, 1080 };
|
|
||||||
|
|
||||||
HeadlessWebView::HeadlessWebView(Core::AnonymousBuffer theme, Gfx::IntSize viewport_size)
|
|
||||||
: m_theme(move(theme))
|
|
||||||
, m_viewport_size(viewport_size)
|
|
||||||
, m_test_promise(TestPromise::construct())
|
|
||||||
{
|
|
||||||
on_new_web_view = [this](auto, auto, Optional<u64> page_index) {
|
|
||||||
if (page_index.has_value()) {
|
|
||||||
auto& web_view = Application::the().create_child_web_view(*this, *page_index);
|
|
||||||
return web_view.handle();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& web_view = Application::the().create_web_view(m_theme, m_viewport_size);
|
|
||||||
return web_view.handle();
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_worker_agent = []() {
|
|
||||||
auto web_worker_paths = MUST(get_paths_for_helper_process("WebWorker"sv));
|
|
||||||
auto worker_client = MUST(launch_web_worker_process(web_worker_paths, Application::request_client()));
|
|
||||||
|
|
||||||
return worker_client->clone_transport();
|
|
||||||
};
|
|
||||||
|
|
||||||
on_reposition_window = [this](auto position) {
|
|
||||||
client().async_set_window_position(m_client_state.page_index, position.template to_type<Web::DevicePixels>());
|
|
||||||
|
|
||||||
client().async_did_update_window_rect(m_client_state.page_index);
|
|
||||||
};
|
|
||||||
|
|
||||||
on_resize_window = [this](auto size) {
|
|
||||||
client().async_set_window_size(m_client_state.page_index, size.template to_type<Web::DevicePixels>());
|
|
||||||
|
|
||||||
client().async_did_update_window_rect(m_client_state.page_index);
|
|
||||||
};
|
|
||||||
|
|
||||||
on_maximize_window = [this]() {
|
|
||||||
client().async_set_window_position(m_client_state.page_index, screen_rect.location());
|
|
||||||
client().async_set_window_size(m_client_state.page_index, screen_rect.size());
|
|
||||||
|
|
||||||
client().async_did_update_window_rect(m_client_state.page_index);
|
|
||||||
};
|
|
||||||
|
|
||||||
on_fullscreen_window = [this]() {
|
|
||||||
client().async_set_window_position(m_client_state.page_index, screen_rect.location());
|
|
||||||
client().async_set_window_size(m_client_state.page_index, screen_rect.size());
|
|
||||||
|
|
||||||
client().async_did_update_window_rect(m_client_state.page_index);
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_alert = [this](auto const&) {
|
|
||||||
m_pending_dialog = Web::Page::PendingDialog::Alert;
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_confirm = [this](auto const&) {
|
|
||||||
m_pending_dialog = Web::Page::PendingDialog::Confirm;
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_prompt = [this](auto const&, auto const& prompt_text) {
|
|
||||||
m_pending_dialog = Web::Page::PendingDialog::Prompt;
|
|
||||||
m_pending_prompt_text = prompt_text;
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_set_prompt_text = [this](auto const& prompt_text) {
|
|
||||||
m_pending_prompt_text = prompt_text;
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_accept_dialog = [this]() {
|
|
||||||
switch (m_pending_dialog) {
|
|
||||||
case Web::Page::PendingDialog::None:
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
break;
|
|
||||||
case Web::Page::PendingDialog::Alert:
|
|
||||||
alert_closed();
|
|
||||||
break;
|
|
||||||
case Web::Page::PendingDialog::Confirm:
|
|
||||||
confirm_closed(true);
|
|
||||||
break;
|
|
||||||
case Web::Page::PendingDialog::Prompt:
|
|
||||||
prompt_closed(move(m_pending_prompt_text));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pending_dialog = Web::Page::PendingDialog::None;
|
|
||||||
};
|
|
||||||
|
|
||||||
on_request_dismiss_dialog = [this]() {
|
|
||||||
switch (m_pending_dialog) {
|
|
||||||
case Web::Page::PendingDialog::None:
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
break;
|
|
||||||
case Web::Page::PendingDialog::Alert:
|
|
||||||
alert_closed();
|
|
||||||
break;
|
|
||||||
case Web::Page::PendingDialog::Confirm:
|
|
||||||
confirm_closed(false);
|
|
||||||
break;
|
|
||||||
case Web::Page::PendingDialog::Prompt:
|
|
||||||
prompt_closed({});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_pending_dialog = Web::Page::PendingDialog::None;
|
|
||||||
m_pending_prompt_text.clear();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
NonnullOwnPtr<HeadlessWebView> HeadlessWebView::create(Core::AnonymousBuffer theme, Gfx::IntSize window_size)
|
|
||||||
{
|
|
||||||
auto view = adopt_own(*new HeadlessWebView(move(theme), window_size));
|
|
||||||
view->initialize_client(CreateNewClient::Yes);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
NonnullOwnPtr<HeadlessWebView> HeadlessWebView::create_child(HeadlessWebView const& parent, u64 page_index)
|
|
||||||
{
|
|
||||||
auto view = adopt_own(*new HeadlessWebView(parent.m_theme, parent.m_viewport_size));
|
|
||||||
|
|
||||||
view->m_client_state.client = parent.client();
|
|
||||||
view->m_client_state.page_index = page_index;
|
|
||||||
view->initialize_client(CreateNewClient::No);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessWebView::initialize_client(CreateNewClient create_new_client)
|
|
||||||
{
|
|
||||||
if (create_new_client == CreateNewClient::Yes) {
|
|
||||||
auto request_server_socket = connect_new_request_server_client(Application::request_client()).release_value_but_fixme_should_propagate_errors();
|
|
||||||
auto image_decoder_socket = connect_new_image_decoder_client(Application::image_decoder_client()).release_value_but_fixme_should_propagate_errors();
|
|
||||||
|
|
||||||
auto web_content_paths = get_paths_for_helper_process("WebContent"sv).release_value_but_fixme_should_propagate_errors();
|
|
||||||
m_client_state.client = launch_web_content_process(*this, web_content_paths, move(image_decoder_socket), move(request_server_socket)).release_value_but_fixme_should_propagate_errors();
|
|
||||||
} else {
|
|
||||||
m_client_state.client->register_view(m_client_state.page_index, *this);
|
|
||||||
}
|
|
||||||
|
|
||||||
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_update_system_theme(m_client_state.page_index, m_theme);
|
|
||||||
client().async_set_system_visibility_state(m_client_state.page_index, true);
|
|
||||||
client().async_set_viewport_size(m_client_state.page_index, viewport_size());
|
|
||||||
client().async_set_window_size(m_client_state.page_index, viewport_size());
|
|
||||||
client().async_update_screen_rects(m_client_state.page_index, { screen_rect }, 0);
|
|
||||||
|
|
||||||
if (Application::chrome_options().allow_popups == WebView::AllowPopups::Yes)
|
|
||||||
client().async_debug_request(m_client_state.page_index, "block-pop-ups"sv, "off"sv);
|
|
||||||
|
|
||||||
if (auto const& web_driver_ipc_path = Application::chrome_options().webdriver_content_ipc_path; web_driver_ipc_path.has_value())
|
|
||||||
client().async_connect_to_webdriver(m_client_state.page_index, *web_driver_ipc_path);
|
|
||||||
|
|
||||||
m_client_state.client->on_web_content_process_crash = [this] {
|
|
||||||
warnln("\033[31;1mWebContent Crashed!!\033[0m");
|
|
||||||
warnln(" Last page loaded: {}", url());
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessWebView::clear_content_filters()
|
|
||||||
{
|
|
||||||
client().async_set_content_filters(m_client_state.page_index, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
NonnullRefPtr<Core::Promise<RefPtr<Gfx::Bitmap>>> HeadlessWebView::take_screenshot()
|
|
||||||
{
|
|
||||||
VERIFY(!m_pending_screenshot);
|
|
||||||
|
|
||||||
m_pending_screenshot = Core::Promise<RefPtr<Gfx::Bitmap>>::construct();
|
|
||||||
client().async_take_document_screenshot(0);
|
|
||||||
|
|
||||||
return *m_pending_screenshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessWebView::did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot)
|
|
||||||
{
|
|
||||||
VERIFY(m_pending_screenshot);
|
|
||||||
|
|
||||||
auto pending_screenshot = move(m_pending_screenshot);
|
|
||||||
pending_screenshot->resolve(screenshot.bitmap());
|
|
||||||
}
|
|
||||||
|
|
||||||
void HeadlessWebView::on_test_complete(TestCompletion completion)
|
|
||||||
{
|
|
||||||
m_test_promise->resolve(move(completion));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Badge.h>
|
|
||||||
#include <AK/RefPtr.h>
|
|
||||||
#include <Ladybird/Headless/Test.h>
|
|
||||||
#include <LibCore/Forward.h>
|
|
||||||
#include <LibCore/Promise.h>
|
|
||||||
#include <LibGfx/Forward.h>
|
|
||||||
#include <LibGfx/Size.h>
|
|
||||||
#include <LibWeb/Page/Page.h>
|
|
||||||
#include <LibWeb/PixelUnits.h>
|
|
||||||
#include <LibWebView/ViewImplementation.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class HeadlessWebView final : public WebView::ViewImplementation {
|
|
||||||
public:
|
|
||||||
static NonnullOwnPtr<HeadlessWebView> create(Core::AnonymousBuffer theme, Gfx::IntSize window_size);
|
|
||||||
static NonnullOwnPtr<HeadlessWebView> create_child(HeadlessWebView const&, u64 page_index);
|
|
||||||
|
|
||||||
void clear_content_filters();
|
|
||||||
|
|
||||||
NonnullRefPtr<Core::Promise<RefPtr<Gfx::Bitmap>>> take_screenshot();
|
|
||||||
|
|
||||||
TestPromise& test_promise() { return *m_test_promise; }
|
|
||||||
void on_test_complete(TestCompletion);
|
|
||||||
|
|
||||||
private:
|
|
||||||
HeadlessWebView(Core::AnonymousBuffer theme, Gfx::IntSize viewport_size);
|
|
||||||
|
|
||||||
void update_zoom() override { }
|
|
||||||
void initialize_client(CreateNewClient) override;
|
|
||||||
|
|
||||||
virtual Web::DevicePixelSize viewport_size() const override { return m_viewport_size.to_type<Web::DevicePixels>(); }
|
|
||||||
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const override { return widget_position; }
|
|
||||||
virtual Gfx::IntPoint to_widget_position(Gfx::IntPoint content_position) const override { return content_position; }
|
|
||||||
|
|
||||||
virtual void did_receive_screenshot(Badge<WebView::WebContentClient>, Gfx::ShareableBitmap const& screenshot) override;
|
|
||||||
|
|
||||||
Core::AnonymousBuffer m_theme;
|
|
||||||
Gfx::IntSize m_viewport_size;
|
|
||||||
|
|
||||||
RefPtr<Core::Promise<RefPtr<Gfx::Bitmap>>> m_pending_screenshot;
|
|
||||||
|
|
||||||
NonnullRefPtr<TestPromise> m_test_promise;
|
|
||||||
|
|
||||||
Web::Page::PendingDialog m_pending_dialog { Web::Page::PendingDialog::None };
|
|
||||||
Optional<String> m_pending_prompt_text;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,542 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AK/ByteBuffer.h>
|
|
||||||
#include <AK/ByteString.h>
|
|
||||||
#include <AK/Enumerate.h>
|
|
||||||
#include <AK/LexicalPath.h>
|
|
||||||
#include <AK/QuickSort.h>
|
|
||||||
#include <AK/Vector.h>
|
|
||||||
#include <Ladybird/Headless/Application.h>
|
|
||||||
#include <Ladybird/Headless/HeadlessWebView.h>
|
|
||||||
#include <Ladybird/Headless/Test.h>
|
|
||||||
#include <LibCore/ConfigFile.h>
|
|
||||||
#include <LibCore/DirIterator.h>
|
|
||||||
#include <LibCore/Directory.h>
|
|
||||||
#include <LibCore/EventLoop.h>
|
|
||||||
#include <LibCore/File.h>
|
|
||||||
#include <LibCore/Timer.h>
|
|
||||||
#include <LibDiff/Format.h>
|
|
||||||
#include <LibDiff/Generator.h>
|
|
||||||
#include <LibFileSystem/FileSystem.h>
|
|
||||||
#include <LibGfx/Bitmap.h>
|
|
||||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
|
||||||
#include <LibURL/URL.h>
|
|
||||||
#include <LibWeb/HTML/SelectedFile.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
static Vector<ByteString> s_skipped_tests;
|
|
||||||
|
|
||||||
static ErrorOr<void> load_test_config(StringView test_root_path)
|
|
||||||
{
|
|
||||||
auto config_path = LexicalPath::join(test_root_path, "TestConfig.ini"sv);
|
|
||||||
auto config_or_error = Core::ConfigFile::open(config_path.string());
|
|
||||||
|
|
||||||
if (config_or_error.is_error()) {
|
|
||||||
if (config_or_error.error().code() == ENOENT)
|
|
||||||
return {};
|
|
||||||
warnln("Unable to open test config {}", config_path);
|
|
||||||
return config_or_error.release_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto config = config_or_error.release_value();
|
|
||||||
|
|
||||||
for (auto const& group : config->groups()) {
|
|
||||||
if (group == "Skipped"sv) {
|
|
||||||
for (auto& key : config->keys(group))
|
|
||||||
s_skipped_tests.append(TRY(FileSystem::real_path(LexicalPath::join(test_root_path, key).string())));
|
|
||||||
} else {
|
|
||||||
warnln("Unknown group '{}' in config {}", group, config_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static ErrorOr<void> collect_dump_tests(Vector<Test>& tests, StringView path, StringView trail, TestMode mode)
|
|
||||||
{
|
|
||||||
Core::DirIterator it(ByteString::formatted("{}/input/{}", path, trail), Core::DirIterator::Flags::SkipDots);
|
|
||||||
|
|
||||||
while (it.has_next()) {
|
|
||||||
auto name = it.next_path();
|
|
||||||
auto input_path = TRY(FileSystem::real_path(ByteString::formatted("{}/input/{}/{}", path, trail, name)));
|
|
||||||
|
|
||||||
if (FileSystem::is_directory(input_path)) {
|
|
||||||
TRY(collect_dump_tests(tests, path, ByteString::formatted("{}/{}", trail, name), mode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name.ends_with(".html"sv) && !name.ends_with(".svg"sv) && !name.ends_with(".xhtml"sv) && !name.ends_with(".xht"sv))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto expectation_path = ByteString::formatted("{}/expected/{}/{}.txt", path, trail, LexicalPath::title(name));
|
|
||||||
tests.append({ mode, input_path, move(expectation_path), {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
static ErrorOr<void> collect_ref_tests(Vector<Test>& tests, StringView path, StringView trail)
|
|
||||||
{
|
|
||||||
Core::DirIterator it(ByteString::formatted("{}/input/{}", path, trail), Core::DirIterator::Flags::SkipDots);
|
|
||||||
while (it.has_next()) {
|
|
||||||
auto name = it.next_path();
|
|
||||||
auto input_path = TRY(FileSystem::real_path(ByteString::formatted("{}/input/{}/{}", path, trail, name)));
|
|
||||||
|
|
||||||
if (FileSystem::is_directory(input_path)) {
|
|
||||||
TRY(collect_ref_tests(tests, path, ByteString::formatted("{}/{}", trail, name)));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
tests.append({ TestMode::Ref, input_path, {}, {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void run_dump_test(HeadlessWebView& view, Test& test, URL::URL const& url, int timeout_in_milliseconds)
|
|
||||||
{
|
|
||||||
auto timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&view, &test]() {
|
|
||||||
view.on_load_finish = {};
|
|
||||||
view.on_text_test_finish = {};
|
|
||||||
|
|
||||||
view.on_test_complete({ test, TestResult::Timeout });
|
|
||||||
});
|
|
||||||
|
|
||||||
auto handle_completed_test = [&test, url]() -> ErrorOr<TestResult> {
|
|
||||||
if (test.expectation_path.is_empty()) {
|
|
||||||
outln("{}", test.text);
|
|
||||||
return TestResult::Pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto open_expectation_file = [&](auto mode) {
|
|
||||||
auto expectation_file_or_error = Core::File::open(test.expectation_path, mode);
|
|
||||||
if (expectation_file_or_error.is_error())
|
|
||||||
warnln("Failed opening '{}': {}", test.expectation_path, expectation_file_or_error.error());
|
|
||||||
|
|
||||||
return expectation_file_or_error;
|
|
||||||
};
|
|
||||||
|
|
||||||
ByteBuffer expectation;
|
|
||||||
|
|
||||||
if (auto expectation_file = open_expectation_file(Core::File::OpenMode::Read); !expectation_file.is_error()) {
|
|
||||||
expectation = TRY(expectation_file.value()->read_until_eof());
|
|
||||||
|
|
||||||
auto result_trimmed = StringView { test.text }.trim("\n"sv, TrimMode::Right);
|
|
||||||
auto expectation_trimmed = StringView { expectation }.trim("\n"sv, TrimMode::Right);
|
|
||||||
|
|
||||||
if (result_trimmed == expectation_trimmed)
|
|
||||||
return TestResult::Pass;
|
|
||||||
} else if (!Application::the().rebaseline) {
|
|
||||||
return expectation_file.release_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Application::the().rebaseline) {
|
|
||||||
auto expectation_file = TRY(open_expectation_file(Core::File::OpenMode::Write));
|
|
||||||
TRY(expectation_file->write_until_depleted(test.text));
|
|
||||||
|
|
||||||
return TestResult::Pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto const color_output = isatty(STDOUT_FILENO) ? Diff::ColorOutput::Yes : Diff::ColorOutput::No;
|
|
||||||
|
|
||||||
if (color_output == Diff::ColorOutput::Yes)
|
|
||||||
outln("\n\033[33;1mTest failed\033[0m: {}", url);
|
|
||||||
else
|
|
||||||
outln("\nTest failed: {}", url);
|
|
||||||
|
|
||||||
auto hunks = TRY(Diff::from_text(expectation, test.text, 3));
|
|
||||||
auto out = TRY(Core::File::standard_output());
|
|
||||||
|
|
||||||
TRY(Diff::write_unified_header(test.expectation_path, test.expectation_path, *out));
|
|
||||||
for (auto const& hunk : hunks)
|
|
||||||
TRY(Diff::write_unified(hunk, *out, color_output));
|
|
||||||
|
|
||||||
return TestResult::Fail;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto on_test_complete = [&view, &test, timer, handle_completed_test]() {
|
|
||||||
timer->stop();
|
|
||||||
|
|
||||||
view.on_load_finish = {};
|
|
||||||
view.on_text_test_finish = {};
|
|
||||||
|
|
||||||
if (auto result = handle_completed_test(); result.is_error())
|
|
||||||
view.on_test_complete({ test, TestResult::Fail });
|
|
||||||
else
|
|
||||||
view.on_test_complete({ test, result.value() });
|
|
||||||
};
|
|
||||||
|
|
||||||
if (test.mode == TestMode::Layout) {
|
|
||||||
view.on_load_finish = [&view, &test, url, on_test_complete = move(on_test_complete)](auto const& loaded_url) {
|
|
||||||
// We don't want subframe loads to trigger the test finish.
|
|
||||||
if (!url.equals(loaded_url, URL::ExcludeFragment::Yes))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// NOTE: We take a screenshot here to force the lazy layout of SVG-as-image documents to happen.
|
|
||||||
// It also causes a lot more code to run, which is good for finding bugs. :^)
|
|
||||||
view.take_screenshot()->when_resolved([&view, &test, on_test_complete = move(on_test_complete)](auto) {
|
|
||||||
auto promise = view.request_internal_page_info(WebView::PageInfoType::LayoutTree | WebView::PageInfoType::PaintTree);
|
|
||||||
|
|
||||||
promise->when_resolved([&test, on_test_complete = move(on_test_complete)](auto const& text) {
|
|
||||||
test.text = text;
|
|
||||||
on_test_complete();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else if (test.mode == TestMode::Text) {
|
|
||||||
view.on_load_finish = [&view, &test, on_test_complete, url](auto const& loaded_url) {
|
|
||||||
// We don't want subframe loads to trigger the test finish.
|
|
||||||
if (!url.equals(loaded_url, URL::ExcludeFragment::Yes))
|
|
||||||
return;
|
|
||||||
|
|
||||||
test.did_finish_loading = true;
|
|
||||||
|
|
||||||
if (test.expectation_path.is_empty()) {
|
|
||||||
auto promise = view.request_internal_page_info(WebView::PageInfoType::Text);
|
|
||||||
|
|
||||||
promise->when_resolved([&test, on_test_complete = move(on_test_complete)](auto const& text) {
|
|
||||||
test.text = text;
|
|
||||||
on_test_complete();
|
|
||||||
});
|
|
||||||
} else if (test.did_finish_test) {
|
|
||||||
on_test_complete();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view.on_text_test_finish = [&test, on_test_complete](auto const& text) {
|
|
||||||
test.text = text;
|
|
||||||
test.did_finish_test = true;
|
|
||||||
|
|
||||||
if (test.did_finish_loading)
|
|
||||||
on_test_complete();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
view.load(url);
|
|
||||||
timer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void run_ref_test(HeadlessWebView& view, Test& test, URL::URL const& url, int timeout_in_milliseconds)
|
|
||||||
{
|
|
||||||
auto timer = Core::Timer::create_single_shot(timeout_in_milliseconds, [&view, &test]() {
|
|
||||||
view.on_load_finish = {};
|
|
||||||
view.on_text_test_finish = {};
|
|
||||||
|
|
||||||
view.on_test_complete({ test, TestResult::Timeout });
|
|
||||||
});
|
|
||||||
|
|
||||||
auto handle_completed_test = [&test, url]() -> ErrorOr<TestResult> {
|
|
||||||
if (test.actual_screenshot->visually_equals(*test.expectation_screenshot))
|
|
||||||
return TestResult::Pass;
|
|
||||||
|
|
||||||
if (Application::the().dump_failed_ref_tests) {
|
|
||||||
warnln("\033[33;1mRef test {} failed; dumping screenshots\033[0m", url);
|
|
||||||
|
|
||||||
auto dump_screenshot = [&](Gfx::Bitmap& bitmap, StringView path) -> ErrorOr<void> {
|
|
||||||
auto screenshot_file = TRY(Core::File::open(path, Core::File::OpenMode::Write));
|
|
||||||
auto encoded_data = TRY(Gfx::PNGWriter::encode(bitmap));
|
|
||||||
TRY(screenshot_file->write_until_depleted(encoded_data));
|
|
||||||
|
|
||||||
outln("\033[33;1mDumped {}\033[0m", TRY(FileSystem::real_path(path)));
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
TRY(Core::Directory::create("test-dumps"sv, Core::Directory::CreateDirectories::Yes));
|
|
||||||
|
|
||||||
auto title = LexicalPath::title(URL::percent_decode(url.serialize_path()));
|
|
||||||
TRY(dump_screenshot(*test.actual_screenshot, ByteString::formatted("test-dumps/{}.png", title)));
|
|
||||||
TRY(dump_screenshot(*test.expectation_screenshot, ByteString::formatted("test-dumps/{}-ref.png", title)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return TestResult::Fail;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto on_test_complete = [&view, &test, timer, handle_completed_test]() {
|
|
||||||
timer->stop();
|
|
||||||
|
|
||||||
view.on_load_finish = {};
|
|
||||||
view.on_text_test_finish = {};
|
|
||||||
|
|
||||||
if (auto result = handle_completed_test(); result.is_error())
|
|
||||||
view.on_test_complete({ test, TestResult::Fail });
|
|
||||||
else
|
|
||||||
view.on_test_complete({ test, result.value() });
|
|
||||||
};
|
|
||||||
|
|
||||||
view.on_load_finish = [&view, &test, on_test_complete = move(on_test_complete)](auto const&) {
|
|
||||||
if (test.actual_screenshot) {
|
|
||||||
view.take_screenshot()->when_resolved([&test, on_test_complete = move(on_test_complete)](RefPtr<Gfx::Bitmap> screenshot) {
|
|
||||||
test.expectation_screenshot = move(screenshot);
|
|
||||||
on_test_complete();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
view.take_screenshot()->when_resolved([&view, &test](RefPtr<Gfx::Bitmap> screenshot) {
|
|
||||||
test.actual_screenshot = move(screenshot);
|
|
||||||
view.debug_request("load-reference-page");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
view.on_text_test_finish = [&](auto const&) {
|
|
||||||
dbgln("Unexpected text test finished during ref test for {}", url);
|
|
||||||
};
|
|
||||||
|
|
||||||
view.load(url);
|
|
||||||
timer->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void run_test(HeadlessWebView& view, Test& test, Application& app)
|
|
||||||
{
|
|
||||||
// Clear the current document.
|
|
||||||
// FIXME: Implement a debug-request to do this more thoroughly.
|
|
||||||
auto promise = Core::Promise<Empty>::construct();
|
|
||||||
|
|
||||||
view.on_load_finish = [promise](auto const& url) {
|
|
||||||
if (!url.equals("about:blank"sv))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Core::deferred_invoke([promise]() {
|
|
||||||
promise->resolve({});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
view.on_text_test_finish = {};
|
|
||||||
|
|
||||||
view.on_request_file_picker = [&](auto const& accepted_file_types, auto allow_multiple_files) {
|
|
||||||
// Create some dummy files for tests.
|
|
||||||
Vector<Web::HTML::SelectedFile> selected_files;
|
|
||||||
|
|
||||||
bool add_txt_files = accepted_file_types.filters.is_empty();
|
|
||||||
bool add_cpp_files = false;
|
|
||||||
|
|
||||||
for (auto const& filter : accepted_file_types.filters) {
|
|
||||||
filter.visit(
|
|
||||||
[](Web::HTML::FileFilter::FileType) {},
|
|
||||||
[&](Web::HTML::FileFilter::MimeType const& mime_type) {
|
|
||||||
if (mime_type.value == "text/plain"sv)
|
|
||||||
add_txt_files = true;
|
|
||||||
},
|
|
||||||
[&](Web::HTML::FileFilter::Extension const& extension) {
|
|
||||||
if (extension.value == "cpp"sv)
|
|
||||||
add_cpp_files = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (add_txt_files) {
|
|
||||||
selected_files.empend("file1"sv, MUST(ByteBuffer::copy("Contents for file1"sv.bytes())));
|
|
||||||
|
|
||||||
if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
|
|
||||||
selected_files.empend("file2"sv, MUST(ByteBuffer::copy("Contents for file2"sv.bytes())));
|
|
||||||
selected_files.empend("file3"sv, MUST(ByteBuffer::copy("Contents for file3"sv.bytes())));
|
|
||||||
selected_files.empend("file4"sv, MUST(ByteBuffer::copy("Contents for file4"sv.bytes())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (add_cpp_files) {
|
|
||||||
selected_files.empend("file1.cpp"sv, MUST(ByteBuffer::copy("int main() {{ return 1; }}"sv.bytes())));
|
|
||||||
|
|
||||||
if (allow_multiple_files == Web::HTML::AllowMultipleFiles::Yes) {
|
|
||||||
selected_files.empend("file2.cpp"sv, MUST(ByteBuffer::copy("int main() {{ return 2; }}"sv.bytes())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view.file_picker_closed(move(selected_files));
|
|
||||||
};
|
|
||||||
|
|
||||||
promise->when_resolved([&view, &test, &app](auto) {
|
|
||||||
auto url = URL::create_with_file_scheme(MUST(FileSystem::real_path(test.input_path)));
|
|
||||||
|
|
||||||
switch (test.mode) {
|
|
||||||
case TestMode::Text:
|
|
||||||
case TestMode::Layout:
|
|
||||||
run_dump_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
|
|
||||||
return;
|
|
||||||
case TestMode::Ref:
|
|
||||||
run_ref_test(view, test, url, app.per_test_timeout_in_seconds * 1000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
});
|
|
||||||
|
|
||||||
view.load("about:blank"sv);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Gfx::IntSize window_size)
|
|
||||||
{
|
|
||||||
auto& app = Application::the();
|
|
||||||
TRY(load_test_config(app.test_root_path));
|
|
||||||
|
|
||||||
Vector<Test> tests;
|
|
||||||
auto test_glob = ByteString::formatted("*{}*", app.test_glob);
|
|
||||||
|
|
||||||
TRY(collect_dump_tests(tests, ByteString::formatted("{}/Layout", app.test_root_path), "."sv, TestMode::Layout));
|
|
||||||
TRY(collect_dump_tests(tests, ByteString::formatted("{}/Text", app.test_root_path), "."sv, TestMode::Text));
|
|
||||||
TRY(collect_ref_tests(tests, ByteString::formatted("{}/Ref", app.test_root_path), "."sv));
|
|
||||||
#if !defined(AK_OS_MACOS)
|
|
||||||
TRY(collect_ref_tests(tests, ByteString::formatted("{}/Screenshot", app.test_root_path), "."sv));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
tests.remove_all_matching([&](auto const& test) {
|
|
||||||
return !test.input_path.matches(test_glob, CaseSensitivity::CaseSensitive);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (app.test_dry_run) {
|
|
||||||
outln("Found {} tests...", tests.size());
|
|
||||||
|
|
||||||
for (auto const& [i, test] : enumerate(tests))
|
|
||||||
outln("{}/{}: {}", i + 1, tests.size(), *LexicalPath::relative_path(test.input_path, app.test_root_path));
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tests.is_empty()) {
|
|
||||||
if (app.test_glob.is_empty())
|
|
||||||
return Error::from_string_literal("No tests found");
|
|
||||||
return Error::from_string_literal("No tests found matching filter");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto concurrency = min(app.test_concurrency, tests.size());
|
|
||||||
size_t loaded_web_views = 0;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < concurrency; ++i) {
|
|
||||||
auto& view = app.create_web_view(theme, window_size);
|
|
||||||
view.on_load_finish = [&](auto const&) { ++loaded_web_views; };
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to wait for the initial about:blank load to complete before starting the tests, otherwise we may load the
|
|
||||||
// test URL before the about:blank load completes. WebContent currently cannot handle this, and will drop the test URL.
|
|
||||||
Core::EventLoop::current().spin_until([&]() {
|
|
||||||
return loaded_web_views == concurrency;
|
|
||||||
});
|
|
||||||
|
|
||||||
size_t pass_count = 0;
|
|
||||||
size_t fail_count = 0;
|
|
||||||
size_t timeout_count = 0;
|
|
||||||
size_t skipped_count = 0;
|
|
||||||
|
|
||||||
bool is_tty = isatty(STDOUT_FILENO);
|
|
||||||
outln("Running {} tests...", tests.size());
|
|
||||||
|
|
||||||
auto all_tests_complete = Core::Promise<Empty>::construct();
|
|
||||||
auto tests_remaining = tests.size();
|
|
||||||
auto current_test = 0uz;
|
|
||||||
|
|
||||||
Vector<TestCompletion> non_passing_tests;
|
|
||||||
|
|
||||||
app.for_each_web_view([&](auto& view) {
|
|
||||||
view.clear_content_filters();
|
|
||||||
|
|
||||||
auto run_next_test = [&]() {
|
|
||||||
auto index = current_test++;
|
|
||||||
if (index >= tests.size())
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto& test = tests[index];
|
|
||||||
test.start_time = UnixDateTime::now();
|
|
||||||
|
|
||||||
if (is_tty) {
|
|
||||||
// Keep clearing and reusing the same line if stdout is a TTY.
|
|
||||||
out("\33[2K\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
out("{}/{}: {}", index + 1, tests.size(), LexicalPath::relative_path(test.input_path, app.test_root_path));
|
|
||||||
|
|
||||||
if (is_tty)
|
|
||||||
fflush(stdout);
|
|
||||||
else
|
|
||||||
outln("");
|
|
||||||
|
|
||||||
Core::deferred_invoke([&]() mutable {
|
|
||||||
if (s_skipped_tests.contains_slow(test.input_path))
|
|
||||||
view.on_test_complete({ test, TestResult::Skipped });
|
|
||||||
else
|
|
||||||
run_test(view, test, app);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
view.test_promise().when_resolved([&, run_next_test](auto result) {
|
|
||||||
result.test.end_time = UnixDateTime::now();
|
|
||||||
|
|
||||||
switch (result.result) {
|
|
||||||
case TestResult::Pass:
|
|
||||||
++pass_count;
|
|
||||||
break;
|
|
||||||
case TestResult::Fail:
|
|
||||||
++fail_count;
|
|
||||||
break;
|
|
||||||
case TestResult::Timeout:
|
|
||||||
++timeout_count;
|
|
||||||
break;
|
|
||||||
case TestResult::Skipped:
|
|
||||||
++skipped_count;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.result != TestResult::Pass)
|
|
||||||
non_passing_tests.append(move(result));
|
|
||||||
|
|
||||||
if (--tests_remaining == 0)
|
|
||||||
all_tests_complete->resolve({});
|
|
||||||
else
|
|
||||||
run_next_test();
|
|
||||||
});
|
|
||||||
|
|
||||||
Core::deferred_invoke([run_next_test]() {
|
|
||||||
run_next_test();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
MUST(all_tests_complete->await());
|
|
||||||
|
|
||||||
if (is_tty)
|
|
||||||
outln("\33[2K\rDone!");
|
|
||||||
|
|
||||||
outln("==================================================");
|
|
||||||
outln("Pass: {}, Fail: {}, Skipped: {}, Timeout: {}", pass_count, fail_count, skipped_count, timeout_count);
|
|
||||||
outln("==================================================");
|
|
||||||
|
|
||||||
for (auto const& non_passing_test : non_passing_tests)
|
|
||||||
outln("{}: {}", test_result_to_string(non_passing_test.result), non_passing_test.test.input_path);
|
|
||||||
|
|
||||||
if (app.log_slowest_tests) {
|
|
||||||
auto tests_to_print = min(10uz, tests.size());
|
|
||||||
outln("\nSlowest {} tests:", tests_to_print);
|
|
||||||
|
|
||||||
quick_sort(tests, [&](auto const& lhs, auto const& rhs) {
|
|
||||||
auto lhs_duration = lhs.end_time - lhs.start_time;
|
|
||||||
auto rhs_duration = rhs.end_time - rhs.start_time;
|
|
||||||
return lhs_duration > rhs_duration;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (auto const& test : tests.span().trim(tests_to_print)) {
|
|
||||||
auto name = LexicalPath::relative_path(test.input_path, app.test_root_path);
|
|
||||||
auto duration = test.end_time - test.start_time;
|
|
||||||
|
|
||||||
outln("{}: {}ms", name, duration.to_milliseconds());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.dump_gc_graph) {
|
|
||||||
app.for_each_web_view([&](auto& view) {
|
|
||||||
if (auto path = view.dump_gc_graph(); path.is_error())
|
|
||||||
warnln("Failed to dump GC graph: {}", path.error());
|
|
||||||
else
|
|
||||||
outln("GC graph dumped to {}", path.value());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.destroy_web_views();
|
|
||||||
|
|
||||||
if (timeout_count == 0 && fail_count == 0)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
return Error::from_string_literal("Failed LibWeb tests");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Assertions.h>
|
|
||||||
#include <AK/ByteString.h>
|
|
||||||
#include <AK/Error.h>
|
|
||||||
#include <AK/RefPtr.h>
|
|
||||||
#include <AK/String.h>
|
|
||||||
#include <AK/StringView.h>
|
|
||||||
#include <AK/Time.h>
|
|
||||||
#include <LibCore/Forward.h>
|
|
||||||
#include <LibCore/Promise.h>
|
|
||||||
#include <LibGfx/Forward.h>
|
|
||||||
#include <LibGfx/Size.h>
|
|
||||||
#include <LibURL/Forward.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class HeadlessWebView;
|
|
||||||
|
|
||||||
enum class TestMode {
|
|
||||||
Layout,
|
|
||||||
Text,
|
|
||||||
Ref,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class TestResult {
|
|
||||||
Pass,
|
|
||||||
Fail,
|
|
||||||
Skipped,
|
|
||||||
Timeout,
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr StringView test_result_to_string(TestResult result)
|
|
||||||
{
|
|
||||||
switch (result) {
|
|
||||||
case TestResult::Pass:
|
|
||||||
return "Pass"sv;
|
|
||||||
case TestResult::Fail:
|
|
||||||
return "Fail"sv;
|
|
||||||
case TestResult::Skipped:
|
|
||||||
return "Skipped"sv;
|
|
||||||
case TestResult::Timeout:
|
|
||||||
return "Timeout"sv;
|
|
||||||
}
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Test {
|
|
||||||
TestMode mode;
|
|
||||||
|
|
||||||
ByteString input_path {};
|
|
||||||
ByteString expectation_path {};
|
|
||||||
|
|
||||||
UnixDateTime start_time {};
|
|
||||||
UnixDateTime end_time {};
|
|
||||||
|
|
||||||
String text {};
|
|
||||||
bool did_finish_test { false };
|
|
||||||
bool did_finish_loading { false };
|
|
||||||
|
|
||||||
RefPtr<Gfx::Bitmap> actual_screenshot {};
|
|
||||||
RefPtr<Gfx::Bitmap> expectation_screenshot {};
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TestCompletion {
|
|
||||||
Test& test;
|
|
||||||
TestResult result;
|
|
||||||
};
|
|
||||||
|
|
||||||
using TestPromise = Core::Promise<TestCompletion>;
|
|
||||||
|
|
||||||
ErrorOr<void> run_tests(Core::AnonymousBuffer const& theme, Gfx::IntSize window_size);
|
|
||||||
void run_dump_test(HeadlessWebView&, Test&, URL::URL const&, int timeout_in_milliseconds);
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
|
||||||
* Copyright (c) 2023-2024, Tim Flynn <trflynn89@ladybird.org>
|
|
||||||
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
|
|
||||||
* Copyright (c) 2023-2024, Sam Atkins <sam@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AK/ByteString.h>
|
|
||||||
#include <AK/LexicalPath.h>
|
|
||||||
#include <AK/Platform.h>
|
|
||||||
#include <AK/String.h>
|
|
||||||
#include <Ladybird/Headless/Application.h>
|
|
||||||
#include <Ladybird/Headless/HeadlessWebView.h>
|
|
||||||
#include <Ladybird/Headless/Test.h>
|
|
||||||
#include <Ladybird/Utilities.h>
|
|
||||||
#include <LibCore/EventLoop.h>
|
|
||||||
#include <LibCore/File.h>
|
|
||||||
#include <LibCore/Promise.h>
|
|
||||||
#include <LibCore/ResourceImplementationFile.h>
|
|
||||||
#include <LibCore/Timer.h>
|
|
||||||
#include <LibFileSystem/FileSystem.h>
|
|
||||||
#include <LibGfx/Bitmap.h>
|
|
||||||
#include <LibGfx/ImageFormats/PNGWriter.h>
|
|
||||||
#include <LibGfx/SystemTheme.h>
|
|
||||||
#include <LibURL/URL.h>
|
|
||||||
|
|
||||||
static ErrorOr<NonnullRefPtr<Core::Timer>> load_page_for_screenshot_and_exit(Core::EventLoop& event_loop, Ladybird::HeadlessWebView& view, URL::URL const& url, int screenshot_timeout)
|
|
||||||
{
|
|
||||||
// FIXME: Allow passing the output path as an argument.
|
|
||||||
static constexpr auto output_file_path = "output.png"sv;
|
|
||||||
|
|
||||||
if (FileSystem::exists(output_file_path))
|
|
||||||
TRY(FileSystem::remove(output_file_path, FileSystem::RecursionMode::Disallowed));
|
|
||||||
|
|
||||||
outln("Taking screenshot after {} seconds", screenshot_timeout);
|
|
||||||
|
|
||||||
auto timer = Core::Timer::create_single_shot(
|
|
||||||
screenshot_timeout * 1000,
|
|
||||||
[&]() {
|
|
||||||
auto promise = view.take_screenshot();
|
|
||||||
|
|
||||||
if (auto screenshot = MUST(promise->await())) {
|
|
||||||
outln("Saving screenshot to {}", output_file_path);
|
|
||||||
|
|
||||||
auto output_file = MUST(Core::File::open(output_file_path, Core::File::OpenMode::Write));
|
|
||||||
auto image_buffer = MUST(Gfx::PNGWriter::encode(*screenshot));
|
|
||||||
MUST(output_file->write_until_depleted(image_buffer.bytes()));
|
|
||||||
} else {
|
|
||||||
warnln("No screenshot available");
|
|
||||||
}
|
|
||||||
|
|
||||||
event_loop.quit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
view.load(url);
|
|
||||||
timer->start();
|
|
||||||
return timer;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|
||||||
{
|
|
||||||
platform_init();
|
|
||||||
|
|
||||||
auto app = Ladybird::Application::create(arguments, "about:newtab"sv);
|
|
||||||
TRY(app->launch_services());
|
|
||||||
|
|
||||||
Core::ResourceImplementation::install(make<Core::ResourceImplementationFile>(MUST(String::from_byte_string(app->resources_folder))));
|
|
||||||
|
|
||||||
auto theme_path = LexicalPath::join(app->resources_folder, "themes"sv, "Default.ini"sv);
|
|
||||||
auto theme = TRY(Gfx::load_system_theme(theme_path.string()));
|
|
||||||
|
|
||||||
// FIXME: Allow passing the window size as an argument.
|
|
||||||
static constexpr Gfx::IntSize window_size { 800, 600 };
|
|
||||||
|
|
||||||
if (!app->test_root_path.is_empty()) {
|
|
||||||
app->test_root_path = LexicalPath::absolute_path(TRY(FileSystem::current_working_directory()), app->test_root_path);
|
|
||||||
TRY(app->launch_test_fixtures());
|
|
||||||
TRY(Ladybird::run_tests(theme, window_size));
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& view = app->create_web_view(move(theme), window_size);
|
|
||||||
|
|
||||||
VERIFY(!WebView::Application::chrome_options().urls.is_empty());
|
|
||||||
auto const& url = WebView::Application::chrome_options().urls.first();
|
|
||||||
if (!url.is_valid()) {
|
|
||||||
warnln("Invalid URL: \"{}\"", url);
|
|
||||||
return Error::from_string_literal("Invalid URL");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app->dump_layout_tree || app->dump_text) {
|
|
||||||
Ladybird::Test test { app->dump_layout_tree ? Ladybird::TestMode::Layout : Ladybird::TestMode::Text };
|
|
||||||
Ladybird::run_dump_test(view, test, url, app->per_test_timeout_in_seconds * 1000);
|
|
||||||
|
|
||||||
auto completion = MUST(view.test_promise().await());
|
|
||||||
return completion.result == Ladybird::TestResult::Pass ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
RefPtr<Core::Timer> timer;
|
|
||||||
if (!WebView::Application::chrome_options().webdriver_content_ipc_path.has_value())
|
|
||||||
timer = TRY(load_page_for_screenshot_and_exit(Core::EventLoop::current(), view, url, app->screenshot_timeout));
|
|
||||||
|
|
||||||
return app->execute();
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "HelperProcess.h"
|
|
||||||
#include "Utilities.h"
|
|
||||||
#include <AK/Enumerate.h>
|
|
||||||
#include <LibCore/Process.h>
|
|
||||||
#include <LibWebView/Application.h>
|
|
||||||
|
|
||||||
template<typename ClientType, typename... ClientArguments>
|
|
||||||
static ErrorOr<NonnullRefPtr<ClientType>> launch_server_process(
|
|
||||||
StringView server_name,
|
|
||||||
ReadonlySpan<ByteString> candidate_server_paths,
|
|
||||||
Vector<ByteString> arguments,
|
|
||||||
ClientArguments&&... client_arguments)
|
|
||||||
{
|
|
||||||
auto process_type = WebView::process_type_from_name(server_name);
|
|
||||||
auto const& chrome_options = WebView::Application::chrome_options();
|
|
||||||
|
|
||||||
if (chrome_options.profile_helper_process == process_type) {
|
|
||||||
arguments.prepend({
|
|
||||||
"--tool=callgrind"sv,
|
|
||||||
"--instr-atstart=no"sv,
|
|
||||||
""sv, // Placeholder for the process path.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chrome_options.debug_helper_process == process_type)
|
|
||||||
arguments.append("--wait-for-debugger"sv);
|
|
||||||
|
|
||||||
for (auto [i, path] : enumerate(candidate_server_paths)) {
|
|
||||||
Core::ProcessSpawnOptions options { .name = server_name, .arguments = arguments };
|
|
||||||
|
|
||||||
if (chrome_options.profile_helper_process == process_type) {
|
|
||||||
options.executable = "valgrind"sv;
|
|
||||||
options.search_for_executable_in_path = true;
|
|
||||||
arguments[2] = path;
|
|
||||||
} else {
|
|
||||||
options.executable = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto result = WebView::Process::spawn<ClientType>(process_type, move(options), forward<ClientArguments>(client_arguments)...);
|
|
||||||
|
|
||||||
if (!result.is_error()) {
|
|
||||||
auto&& [process, client] = result.release_value();
|
|
||||||
|
|
||||||
if constexpr (requires { client->set_pid(pid_t {}); })
|
|
||||||
client->set_pid(process.pid());
|
|
||||||
|
|
||||||
WebView::Application::the().add_child_process(move(process));
|
|
||||||
|
|
||||||
if (chrome_options.profile_helper_process == process_type) {
|
|
||||||
dbgln();
|
|
||||||
dbgln("\033[1;45mLaunched {} process under callgrind!\033[0m", server_name);
|
|
||||||
dbgln("\033[100mRun `\033[4mcallgrind_control -i on\033[24m` to start instrumentation and `\033[4mcallgrind_control -i off\033[24m` stop it again.\033[0m");
|
|
||||||
dbgln();
|
|
||||||
}
|
|
||||||
|
|
||||||
return move(client);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i == candidate_server_paths.size() - 1) {
|
|
||||||
warnln("Could not launch any of {}: {}", candidate_server_paths, result.error());
|
|
||||||
return result.release_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process(
|
|
||||||
WebView::ViewImplementation& view,
|
|
||||||
ReadonlySpan<ByteString> candidate_web_content_paths,
|
|
||||||
IPC::File image_decoder_socket,
|
|
||||||
Optional<IPC::File> request_server_socket)
|
|
||||||
{
|
|
||||||
auto const& web_content_options = WebView::Application::web_content_options();
|
|
||||||
|
|
||||||
Vector<ByteString> arguments {
|
|
||||||
"--command-line"sv,
|
|
||||||
web_content_options.command_line.to_byte_string(),
|
|
||||||
"--executable-path"sv,
|
|
||||||
web_content_options.executable_path.to_byte_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (web_content_options.config_path.has_value()) {
|
|
||||||
arguments.append("--config-path"sv);
|
|
||||||
arguments.append(web_content_options.config_path.value());
|
|
||||||
}
|
|
||||||
if (web_content_options.is_layout_test_mode == WebView::IsLayoutTestMode::Yes)
|
|
||||||
arguments.append("--layout-test-mode"sv);
|
|
||||||
if (web_content_options.log_all_js_exceptions == WebView::LogAllJSExceptions::Yes)
|
|
||||||
arguments.append("--log-all-js-exceptions"sv);
|
|
||||||
if (web_content_options.enable_idl_tracing == WebView::EnableIDLTracing::Yes)
|
|
||||||
arguments.append("--enable-idl-tracing"sv);
|
|
||||||
if (web_content_options.enable_http_cache == WebView::EnableHTTPCache::Yes)
|
|
||||||
arguments.append("--enable-http-cache"sv);
|
|
||||||
if (web_content_options.expose_internals_object == WebView::ExposeInternalsObject::Yes)
|
|
||||||
arguments.append("--expose-internals-object"sv);
|
|
||||||
if (web_content_options.force_cpu_painting == WebView::ForceCPUPainting::Yes)
|
|
||||||
arguments.append("--force-cpu-painting"sv);
|
|
||||||
if (web_content_options.force_fontconfig == WebView::ForceFontconfig::Yes)
|
|
||||||
arguments.append("--force-fontconfig"sv);
|
|
||||||
if (web_content_options.collect_garbage_on_every_allocation == WebView::CollectGarbageOnEveryAllocation::Yes)
|
|
||||||
arguments.append("--collect-garbage-on-every-allocation"sv);
|
|
||||||
|
|
||||||
if (auto server = mach_server_name(); server.has_value()) {
|
|
||||||
arguments.append("--mach-server-name"sv);
|
|
||||||
arguments.append(server.value());
|
|
||||||
}
|
|
||||||
if (request_server_socket.has_value()) {
|
|
||||||
arguments.append("--request-server-socket"sv);
|
|
||||||
arguments.append(ByteString::number(request_server_socket->fd()));
|
|
||||||
}
|
|
||||||
|
|
||||||
arguments.append("--image-decoder-socket"sv);
|
|
||||||
arguments.append(ByteString::number(image_decoder_socket.fd()));
|
|
||||||
|
|
||||||
return launch_server_process<WebView::WebContentClient>("WebContent"sv, candidate_web_content_paths, move(arguments), view);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> launch_image_decoder_process(ReadonlySpan<ByteString> candidate_image_decoder_paths)
|
|
||||||
{
|
|
||||||
Vector<ByteString> arguments;
|
|
||||||
if (auto server = mach_server_name(); server.has_value()) {
|
|
||||||
arguments.append("--mach-server-name"sv);
|
|
||||||
arguments.append(server.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
return launch_server_process<ImageDecoderClient::Client>("ImageDecoder"sv, candidate_image_decoder_paths, arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(ReadonlySpan<ByteString> candidate_web_worker_paths, NonnullRefPtr<Requests::RequestClient> request_client)
|
|
||||||
{
|
|
||||||
Vector<ByteString> arguments;
|
|
||||||
|
|
||||||
auto socket = TRY(connect_new_request_server_client(*request_client));
|
|
||||||
arguments.append("--request-server-socket"sv);
|
|
||||||
arguments.append(ByteString::number(socket.fd()));
|
|
||||||
|
|
||||||
return launch_server_process<Web::HTML::WebWorkerClient>("WebWorker"sv, candidate_web_worker_paths, move(arguments));
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<Requests::RequestClient>> launch_request_server_process(ReadonlySpan<ByteString> candidate_request_server_paths, StringView serenity_resource_root)
|
|
||||||
{
|
|
||||||
Vector<ByteString> arguments;
|
|
||||||
|
|
||||||
if (!serenity_resource_root.is_empty()) {
|
|
||||||
arguments.append("--serenity-resource-root"sv);
|
|
||||||
arguments.append(serenity_resource_root);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto const& certificate : WebView::Application::chrome_options().certificates)
|
|
||||||
arguments.append(ByteString::formatted("--certificate={}", certificate));
|
|
||||||
|
|
||||||
if (auto server = mach_server_name(); server.has_value()) {
|
|
||||||
arguments.append("--mach-server-name"sv);
|
|
||||||
arguments.append(server.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
return launch_server_process<Requests::RequestClient>("RequestServer"sv, candidate_request_server_paths, move(arguments));
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<IPC::File> connect_new_request_server_client(Requests::RequestClient& client)
|
|
||||||
{
|
|
||||||
auto new_socket = client.send_sync_but_allow_failure<Messages::RequestServer::ConnectNewClient>();
|
|
||||||
if (!new_socket)
|
|
||||||
return Error::from_string_literal("Failed to connect to RequestServer");
|
|
||||||
|
|
||||||
auto socket = new_socket->take_client_socket();
|
|
||||||
TRY(socket.clear_close_on_exec());
|
|
||||||
|
|
||||||
return socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<IPC::File> connect_new_image_decoder_client(ImageDecoderClient::Client& client)
|
|
||||||
{
|
|
||||||
auto new_socket = client.send_sync_but_allow_failure<Messages::ImageDecoderServer::ConnectNewClients>(1);
|
|
||||||
if (!new_socket)
|
|
||||||
return Error::from_string_literal("Failed to connect to ImageDecoder");
|
|
||||||
|
|
||||||
auto sockets = new_socket->take_sockets();
|
|
||||||
if (sockets.size() != 1)
|
|
||||||
return Error::from_string_literal("Failed to connect to ImageDecoder");
|
|
||||||
|
|
||||||
auto socket = sockets.take_last();
|
|
||||||
TRY(socket.clear_close_on_exec());
|
|
||||||
|
|
||||||
return socket;
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Error.h>
|
|
||||||
#include <AK/Optional.h>
|
|
||||||
#include <AK/Span.h>
|
|
||||||
#include <AK/StringView.h>
|
|
||||||
#include <LibImageDecoderClient/Client.h>
|
|
||||||
#include <LibRequests/RequestClient.h>
|
|
||||||
#include <LibWeb/Worker/WebWorkerClient.h>
|
|
||||||
#include <LibWebView/ViewImplementation.h>
|
|
||||||
#include <LibWebView/WebContentClient.h>
|
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<WebView::WebContentClient>> launch_web_content_process(
|
|
||||||
WebView::ViewImplementation& view,
|
|
||||||
ReadonlySpan<ByteString> candidate_web_content_paths,
|
|
||||||
IPC::File image_decoder_socket,
|
|
||||||
Optional<IPC::File> request_server_socket = {});
|
|
||||||
|
|
||||||
ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> launch_image_decoder_process(ReadonlySpan<ByteString> candidate_image_decoder_paths);
|
|
||||||
ErrorOr<NonnullRefPtr<Web::HTML::WebWorkerClient>> launch_web_worker_process(ReadonlySpan<ByteString> candidate_web_worker_paths, NonnullRefPtr<Requests::RequestClient>);
|
|
||||||
ErrorOr<NonnullRefPtr<Requests::RequestClient>> launch_request_server_process(ReadonlySpan<ByteString> candidate_request_server_paths, StringView serenity_resource_root);
|
|
||||||
|
|
||||||
ErrorOr<IPC::File> connect_new_request_server_client(Requests::RequestClient&);
|
|
||||||
ErrorOr<IPC::File> connect_new_image_decoder_client(ImageDecoderClient::Client&);
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
|
||||||
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "ImageCodecPlugin.h"
|
|
||||||
#include "Utilities.h"
|
|
||||||
#include <LibGfx/Bitmap.h>
|
|
||||||
#include <LibGfx/ImageFormats/ImageDecoder.h>
|
|
||||||
#include <LibImageDecoderClient/Client.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
ImageCodecPlugin::ImageCodecPlugin(NonnullRefPtr<ImageDecoderClient::Client> client)
|
|
||||||
: m_client(move(client))
|
|
||||||
{
|
|
||||||
m_client->on_death = [this] {
|
|
||||||
m_client = nullptr;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImageCodecPlugin::set_client(NonnullRefPtr<ImageDecoderClient::Client> client)
|
|
||||||
{
|
|
||||||
m_client = move(client);
|
|
||||||
m_client->on_death = [this] {
|
|
||||||
m_client = nullptr;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageCodecPlugin::~ImageCodecPlugin() = default;
|
|
||||||
|
|
||||||
NonnullRefPtr<Core::Promise<Web::Platform::DecodedImage>> ImageCodecPlugin::decode_image(ReadonlyBytes bytes, Function<ErrorOr<void>(Web::Platform::DecodedImage&)> on_resolved, Function<void(Error&)> on_rejected)
|
|
||||||
{
|
|
||||||
auto promise = Core::Promise<Web::Platform::DecodedImage>::construct();
|
|
||||||
if (on_resolved)
|
|
||||||
promise->on_resolution = move(on_resolved);
|
|
||||||
if (on_rejected)
|
|
||||||
promise->on_rejection = move(on_rejected);
|
|
||||||
|
|
||||||
if (!m_client) {
|
|
||||||
promise->reject(Error::from_string_literal("ImageDecoderClient is disconnected"));
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto image_decoder_promise = m_client->decode_image(
|
|
||||||
bytes,
|
|
||||||
[promise](ImageDecoderClient::DecodedImage& result) -> ErrorOr<void> {
|
|
||||||
// FIXME: Remove this codec plugin and just use the ImageDecoderClient directly to avoid these copies
|
|
||||||
Web::Platform::DecodedImage decoded_image;
|
|
||||||
decoded_image.is_animated = result.is_animated;
|
|
||||||
decoded_image.loop_count = result.loop_count;
|
|
||||||
for (auto& frame : result.frames) {
|
|
||||||
decoded_image.frames.empend(move(frame.bitmap), frame.duration);
|
|
||||||
}
|
|
||||||
promise->resolve(move(decoded_image));
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
[promise](auto& error) {
|
|
||||||
promise->reject(Error::copy(error));
|
|
||||||
});
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022, Dex♪ <dexes.ttp@gmail.com>
|
|
||||||
* Copyright (c) 2022, Andreas Kling <andreas@ladybird.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <LibImageDecoderClient/Client.h>
|
|
||||||
#include <LibWeb/Platform/ImageCodecPlugin.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class ImageCodecPlugin final : public Web::Platform::ImageCodecPlugin {
|
|
||||||
public:
|
|
||||||
explicit ImageCodecPlugin(NonnullRefPtr<ImageDecoderClient::Client>);
|
|
||||||
virtual ~ImageCodecPlugin() override;
|
|
||||||
|
|
||||||
virtual NonnullRefPtr<Core::Promise<Web::Platform::DecodedImage>> decode_image(ReadonlyBytes, Function<ErrorOr<void>(Web::Platform::DecodedImage&)> on_resolved, Function<void(Error&)> on_rejected) override;
|
|
||||||
|
|
||||||
void set_client(NonnullRefPtr<ImageDecoderClient::Client>);
|
|
||||||
|
|
||||||
private:
|
|
||||||
RefPtr<ImageDecoderClient::Client> m_client;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,105 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "MachPortServer.h"
|
|
||||||
#include <AK/Debug.h>
|
|
||||||
#include <LibCore/Platform/MachMessageTypes.h>
|
|
||||||
#include <LibCore/Platform/ProcessStatisticsMach.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
MachPortServer::MachPortServer()
|
|
||||||
: m_thread(Threading::Thread::construct([this]() -> intptr_t { thread_loop(); return 0; }, "MachPortServer"sv))
|
|
||||||
, m_server_port_name(ByteString::formatted("org.ladybird.Ladybird.helper.{}", getpid()))
|
|
||||||
{
|
|
||||||
if (auto err = allocate_server_port(); err.is_error())
|
|
||||||
dbgln("Failed to allocate server port: {}", err.error());
|
|
||||||
else
|
|
||||||
start();
|
|
||||||
}
|
|
||||||
|
|
||||||
MachPortServer::~MachPortServer()
|
|
||||||
{
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MachPortServer::start()
|
|
||||||
{
|
|
||||||
m_thread->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MachPortServer::stop()
|
|
||||||
{
|
|
||||||
// FIXME: We should join instead (after storing should_stop = false) when we have a way to interrupt the thread's mach_msg call
|
|
||||||
m_thread->detach();
|
|
||||||
m_should_stop.store(true, MemoryOrder::memory_order_release);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MachPortServer::is_initialized()
|
|
||||||
{
|
|
||||||
return MACH_PORT_VALID(m_server_port_recv_right.port()) && MACH_PORT_VALID(m_server_port_send_right.port());
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> MachPortServer::allocate_server_port()
|
|
||||||
{
|
|
||||||
m_server_port_recv_right = TRY(Core::MachPort::create_with_right(Core::MachPort::PortRight::Receive));
|
|
||||||
m_server_port_send_right = TRY(m_server_port_recv_right.insert_right(Core::MachPort::MessageRight::MakeSend));
|
|
||||||
TRY(m_server_port_recv_right.register_with_bootstrap_server(m_server_port_name));
|
|
||||||
|
|
||||||
dbgln_if(MACH_PORT_DEBUG, "Success! we created and attached mach port {:x} to bootstrap server with name {}", m_server_port_recv_right.port(), m_server_port_name);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void MachPortServer::thread_loop()
|
|
||||||
{
|
|
||||||
while (!m_should_stop.load(MemoryOrder::memory_order_acquire)) {
|
|
||||||
Core::Platform::ReceivedMachMessage message {};
|
|
||||||
|
|
||||||
// Get the pid of the child from the audit trailer so we can associate the port w/it
|
|
||||||
mach_msg_options_t const options = MACH_RCV_MSG | MACH_RCV_TRAILER_TYPE(MACH_RCV_TRAILER_AUDIT) | MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT);
|
|
||||||
|
|
||||||
// FIXME: How can we interrupt this call during application shutdown?
|
|
||||||
auto const ret = mach_msg(&message.header, options, 0, sizeof(message), m_server_port_recv_right.port(), MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
|
|
||||||
if (ret != KERN_SUCCESS) {
|
|
||||||
dbgln("mach_msg failed: {}", mach_error_string(ret));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.header.msgh_id == Core::Platform::BACKING_STORE_IOSURFACES_MESSAGE_ID) {
|
|
||||||
auto pid = static_cast<pid_t>(message.body.parent_iosurface.trailer.msgh_audit.val[5]);
|
|
||||||
auto const& backing_stores_message = message.body.parent_iosurface;
|
|
||||||
auto front_child_port = Core::MachPort::adopt_right(backing_stores_message.front_descriptor.name, Core::MachPort::PortRight::Send);
|
|
||||||
auto back_child_port = Core::MachPort::adopt_right(backing_stores_message.back_descriptor.name, Core::MachPort::PortRight::Send);
|
|
||||||
auto const& metadata = backing_stores_message.metadata;
|
|
||||||
if (on_receive_backing_stores)
|
|
||||||
on_receive_backing_stores({ .pid = pid,
|
|
||||||
.page_id = metadata.page_id,
|
|
||||||
.front_backing_store_id = metadata.front_backing_store_id,
|
|
||||||
.back_backing_store_id = metadata.back_backing_store_id,
|
|
||||||
.front_backing_store_port = move(front_child_port),
|
|
||||||
.back_backing_store_port = move(back_child_port) });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.header.msgh_id == Core::Platform::SELF_TASK_PORT_MESSAGE_ID) {
|
|
||||||
if (MACH_MSGH_BITS_LOCAL(message.header.msgh_bits) != MACH_MSG_TYPE_MOVE_SEND) {
|
|
||||||
dbgln("Received message with invalid local port rights {}, ignoring", MACH_MSGH_BITS_LOCAL(message.header.msgh_bits));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto const& task_port_message = message.body.parent;
|
|
||||||
auto pid = static_cast<pid_t>(task_port_message.trailer.msgh_audit.val[5]);
|
|
||||||
auto child_port = Core::MachPort::adopt_right(task_port_message.port_descriptor.name, Core::MachPort::PortRight::Send);
|
|
||||||
dbgln_if(MACH_PORT_DEBUG, "Received child port {:x} from pid {}", child_port.port(), pid);
|
|
||||||
if (on_receive_child_mach_port)
|
|
||||||
on_receive_child_mach_port(pid, move(child_port));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbgln("Received message with id {}, ignoring", message.header.msgh_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Platform.h>
|
|
||||||
|
|
||||||
#if !defined(AK_OS_MACH)
|
|
||||||
# error "This file is only for Mach kernel-based OS's"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <AK/Atomic.h>
|
|
||||||
#include <AK/String.h>
|
|
||||||
#include <LibCore/MachPort.h>
|
|
||||||
#include <LibThreading/Thread.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class MachPortServer {
|
|
||||||
|
|
||||||
public:
|
|
||||||
MachPortServer();
|
|
||||||
~MachPortServer();
|
|
||||||
|
|
||||||
void start();
|
|
||||||
void stop();
|
|
||||||
|
|
||||||
bool is_initialized();
|
|
||||||
|
|
||||||
Function<void(pid_t, Core::MachPort)> on_receive_child_mach_port;
|
|
||||||
struct BackingStoresMessage {
|
|
||||||
pid_t pid { -1 };
|
|
||||||
u64 page_id { 0 };
|
|
||||||
i32 front_backing_store_id { 0 };
|
|
||||||
i32 back_backing_store_id { 0 };
|
|
||||||
Core::MachPort front_backing_store_port;
|
|
||||||
Core::MachPort back_backing_store_port;
|
|
||||||
};
|
|
||||||
Function<void(BackingStoresMessage)> on_receive_backing_stores;
|
|
||||||
|
|
||||||
ByteString const& server_port_name() const { return m_server_port_name; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
void thread_loop();
|
|
||||||
ErrorOr<void> allocate_server_port();
|
|
||||||
|
|
||||||
NonnullRefPtr<Threading::Thread> m_thread;
|
|
||||||
ByteString const m_server_port_name;
|
|
||||||
Core::MachPort m_server_port_recv_right;
|
|
||||||
Core::MachPort m_server_port_send_right;
|
|
||||||
|
|
||||||
Atomic<bool> m_should_stop { false };
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <Ladybird/HelperProcess.h>
|
|
||||||
#include <Ladybird/Qt/Application.h>
|
|
||||||
#include <Ladybird/Qt/Settings.h>
|
|
||||||
#include <Ladybird/Qt/StringUtils.h>
|
|
||||||
#include <Ladybird/Qt/TaskManagerWindow.h>
|
|
||||||
#include <Ladybird/Utilities.h>
|
|
||||||
#include <LibCore/ArgsParser.h>
|
|
||||||
#include <LibWebView/URL.h>
|
|
||||||
#include <QFileDialog>
|
|
||||||
#include <QFileOpenEvent>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
Application::Application(Badge<WebView::Application>, Main::Arguments& arguments)
|
|
||||||
: QApplication(arguments.argc, arguments.argv)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::create_platform_options(WebView::ChromeOptions&, WebView::WebContentOptions& web_content_options)
|
|
||||||
{
|
|
||||||
web_content_options.config_path = Settings::the()->directory();
|
|
||||||
}
|
|
||||||
|
|
||||||
Application::~Application()
|
|
||||||
{
|
|
||||||
close_task_manager_window();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Application::event(QEvent* event)
|
|
||||||
{
|
|
||||||
switch (event->type()) {
|
|
||||||
case QEvent::FileOpen: {
|
|
||||||
if (!on_open_file)
|
|
||||||
break;
|
|
||||||
|
|
||||||
auto const& open_event = *static_cast<QFileOpenEvent const*>(event);
|
|
||||||
auto file = ak_string_from_qstring(open_event.file());
|
|
||||||
|
|
||||||
if (auto file_url = WebView::sanitize_url(file); file_url.has_value())
|
|
||||||
on_open_file(file_url.release_value());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QApplication::event(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ErrorOr<NonnullRefPtr<ImageDecoderClient::Client>> launch_new_image_decoder()
|
|
||||||
{
|
|
||||||
auto paths = TRY(get_paths_for_helper_process("ImageDecoder"sv));
|
|
||||||
return launch_image_decoder_process(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> Application::initialize_image_decoder()
|
|
||||||
{
|
|
||||||
m_image_decoder_client = TRY(launch_new_image_decoder());
|
|
||||||
|
|
||||||
m_image_decoder_client->on_death = [this] {
|
|
||||||
m_image_decoder_client = nullptr;
|
|
||||||
if (auto err = this->initialize_image_decoder(); 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 {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::show_task_manager_window()
|
|
||||||
{
|
|
||||||
if (!m_task_manager_window) {
|
|
||||||
m_task_manager_window = new TaskManagerWindow(nullptr);
|
|
||||||
}
|
|
||||||
m_task_manager_window->show();
|
|
||||||
m_task_manager_window->activateWindow();
|
|
||||||
m_task_manager_window->raise();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::close_task_manager_window()
|
|
||||||
{
|
|
||||||
if (m_task_manager_window) {
|
|
||||||
m_task_manager_window->close();
|
|
||||||
delete m_task_manager_window;
|
|
||||||
m_task_manager_window = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BrowserWindow& Application::new_window(Vector<URL::URL> const& initial_urls, BrowserWindow::IsPopupWindow is_popup_window, Tab* parent_tab, Optional<u64> page_index)
|
|
||||||
{
|
|
||||||
auto* window = new BrowserWindow(initial_urls, is_popup_window, parent_tab, move(page_index));
|
|
||||||
set_active_window(*window);
|
|
||||||
window->show();
|
|
||||||
if (initial_urls.is_empty()) {
|
|
||||||
auto* tab = window->current_tab();
|
|
||||||
if (tab) {
|
|
||||||
tab->set_url_is_hidden(true);
|
|
||||||
tab->focus_location_editor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window->activateWindow();
|
|
||||||
window->raise();
|
|
||||||
return *window;
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<ByteString> Application::ask_user_for_download_folder() const
|
|
||||||
{
|
|
||||||
auto path = QFileDialog::getExistingDirectory(nullptr, "Select download directory", QDir::homePath());
|
|
||||||
if (path.isNull())
|
|
||||||
return {};
|
|
||||||
|
|
||||||
return ak_byte_string_from_qstring(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2024, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Function.h>
|
|
||||||
#include <AK/HashTable.h>
|
|
||||||
#include <Ladybird/Qt/BrowserWindow.h>
|
|
||||||
#include <LibImageDecoderClient/Client.h>
|
|
||||||
#include <LibRequests/RequestClient.h>
|
|
||||||
#include <LibURL/URL.h>
|
|
||||||
#include <LibWebView/Application.h>
|
|
||||||
#include <QApplication>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class Application
|
|
||||||
: public QApplication
|
|
||||||
, public WebView::Application {
|
|
||||||
Q_OBJECT
|
|
||||||
WEB_VIEW_APPLICATION(Application)
|
|
||||||
|
|
||||||
public:
|
|
||||||
virtual ~Application() override;
|
|
||||||
|
|
||||||
virtual bool event(QEvent* event) override;
|
|
||||||
|
|
||||||
Function<void(URL::URL)> on_open_file;
|
|
||||||
RefPtr<Requests::RequestClient> request_server_client;
|
|
||||||
|
|
||||||
NonnullRefPtr<ImageDecoderClient::Client> image_decoder_client() const { return *m_image_decoder_client; }
|
|
||||||
ErrorOr<void> initialize_image_decoder();
|
|
||||||
|
|
||||||
BrowserWindow& new_window(Vector<URL::URL> const& initial_urls, BrowserWindow::IsPopupWindow is_popup_window = BrowserWindow::IsPopupWindow::No, Tab* parent_tab = nullptr, Optional<u64> page_index = {});
|
|
||||||
|
|
||||||
void show_task_manager_window();
|
|
||||||
void close_task_manager_window();
|
|
||||||
|
|
||||||
BrowserWindow& active_window() { return *m_active_window; }
|
|
||||||
void set_active_window(BrowserWindow& w) { m_active_window = &w; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
virtual void create_platform_options(WebView::ChromeOptions&, WebView::WebContentOptions&) override;
|
|
||||||
|
|
||||||
virtual Optional<ByteString> ask_user_for_download_folder() const override;
|
|
||||||
|
|
||||||
TaskManagerWindow* m_task_manager_window { nullptr };
|
|
||||||
BrowserWindow* m_active_window { nullptr };
|
|
||||||
|
|
||||||
RefPtr<ImageDecoderClient::Client> m_image_decoder_client;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "AudioCodecPluginQt.h"
|
|
||||||
#include "AudioThread.h"
|
|
||||||
#include <LibMedia/Audio/Loader.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
ErrorOr<NonnullOwnPtr<AudioCodecPluginQt>> AudioCodecPluginQt::create(NonnullRefPtr<Audio::Loader> loader)
|
|
||||||
{
|
|
||||||
auto audio_thread = TRY(AudioThread::create(move(loader)));
|
|
||||||
audio_thread->start();
|
|
||||||
|
|
||||||
return adopt_nonnull_own_or_enomem(new (nothrow) AudioCodecPluginQt(move(audio_thread)));
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioCodecPluginQt::AudioCodecPluginQt(NonnullOwnPtr<AudioThread> audio_thread)
|
|
||||||
: m_audio_thread(move(audio_thread))
|
|
||||||
{
|
|
||||||
connect(m_audio_thread, &AudioThread::playback_position_updated, this, [this](auto position) {
|
|
||||||
if (on_playback_position_updated)
|
|
||||||
on_playback_position_updated(position);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioCodecPluginQt::~AudioCodecPluginQt()
|
|
||||||
{
|
|
||||||
m_audio_thread->stop().release_value_but_fixme_should_propagate_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioCodecPluginQt::resume_playback()
|
|
||||||
{
|
|
||||||
m_audio_thread->queue_task({ AudioTask::Type::Play }).release_value_but_fixme_should_propagate_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioCodecPluginQt::pause_playback()
|
|
||||||
{
|
|
||||||
m_audio_thread->queue_task({ AudioTask::Type::Pause }).release_value_but_fixme_should_propagate_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioCodecPluginQt::set_volume(double volume)
|
|
||||||
{
|
|
||||||
|
|
||||||
AudioTask task { AudioTask::Type::Volume };
|
|
||||||
task.data = volume;
|
|
||||||
|
|
||||||
m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioCodecPluginQt::seek(double position)
|
|
||||||
{
|
|
||||||
AudioTask task { AudioTask::Type::Seek };
|
|
||||||
task.data = position;
|
|
||||||
|
|
||||||
m_audio_thread->queue_task(move(task)).release_value_but_fixme_should_propagate_errors();
|
|
||||||
}
|
|
||||||
|
|
||||||
AK::Duration AudioCodecPluginQt::duration()
|
|
||||||
{
|
|
||||||
return m_audio_thread->duration();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Error.h>
|
|
||||||
#include <AK/NonnullOwnPtr.h>
|
|
||||||
#include <AK/NonnullRefPtr.h>
|
|
||||||
#include <LibMedia/Audio/Forward.h>
|
|
||||||
#include <LibWeb/Platform/AudioCodecPlugin.h>
|
|
||||||
#include <QObject>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class AudioThread;
|
|
||||||
|
|
||||||
class AudioCodecPluginQt final
|
|
||||||
: public QObject
|
|
||||||
, public Web::Platform::AudioCodecPlugin {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
static ErrorOr<NonnullOwnPtr<AudioCodecPluginQt>> create(NonnullRefPtr<Audio::Loader>);
|
|
||||||
virtual ~AudioCodecPluginQt() override;
|
|
||||||
|
|
||||||
virtual void resume_playback() override;
|
|
||||||
virtual void pause_playback() override;
|
|
||||||
virtual void set_volume(double) override;
|
|
||||||
virtual void seek(double) override;
|
|
||||||
|
|
||||||
virtual AK::Duration duration() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit AudioCodecPluginQt(NonnullOwnPtr<AudioThread>);
|
|
||||||
|
|
||||||
NonnullOwnPtr<AudioThread> m_audio_thread;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,212 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "AudioThread.h"
|
|
||||||
#include <LibWeb/Platform/AudioCodecPlugin.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
struct AudioDevice {
|
|
||||||
static AudioDevice create(Audio::Loader const& loader)
|
|
||||||
{
|
|
||||||
auto const& device_info = QMediaDevices::defaultAudioOutput();
|
|
||||||
|
|
||||||
auto format = device_info.preferredFormat();
|
|
||||||
format.setSampleRate(static_cast<int>(loader.sample_rate()));
|
|
||||||
format.setChannelCount(2);
|
|
||||||
|
|
||||||
auto audio_output = make<QAudioSink>(device_info, format);
|
|
||||||
return AudioDevice { move(audio_output) };
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioDevice(AudioDevice&&) = default;
|
|
||||||
|
|
||||||
AudioDevice& operator=(AudioDevice&& device)
|
|
||||||
{
|
|
||||||
if (audio_output) {
|
|
||||||
audio_output->stop();
|
|
||||||
io_device = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
swap(audio_output, device.audio_output);
|
|
||||||
swap(io_device, device.io_device);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
~AudioDevice()
|
|
||||||
{
|
|
||||||
if (audio_output)
|
|
||||||
audio_output->stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
OwnPtr<QAudioSink> audio_output;
|
|
||||||
QIODevice* io_device { nullptr };
|
|
||||||
|
|
||||||
private:
|
|
||||||
explicit AudioDevice(NonnullOwnPtr<QAudioSink> output)
|
|
||||||
: audio_output(move(output))
|
|
||||||
{
|
|
||||||
io_device = audio_output->start();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ErrorOr<NonnullOwnPtr<AudioThread>> AudioThread::create(NonnullRefPtr<Audio::Loader> loader)
|
|
||||||
{
|
|
||||||
auto task_queue = TRY(AudioTaskQueue::create());
|
|
||||||
return adopt_nonnull_own_or_enomem(new (nothrow) AudioThread(move(loader), move(task_queue)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> AudioThread::stop()
|
|
||||||
{
|
|
||||||
TRY(queue_task({ AudioTask::Type::Stop }));
|
|
||||||
wait();
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> AudioThread::queue_task(AudioTask task)
|
|
||||||
{
|
|
||||||
return m_task_queue.blocking_enqueue(move(task), []() {
|
|
||||||
usleep(UPDATE_RATE_MS * 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioThread::AudioThread(NonnullRefPtr<Audio::Loader> loader, AudioTaskQueue task_queue)
|
|
||||||
: m_loader(move(loader))
|
|
||||||
, m_task_queue(move(task_queue))
|
|
||||||
{
|
|
||||||
auto duration = static_cast<double>(m_loader->total_samples()) / static_cast<double>(m_loader->sample_rate());
|
|
||||||
m_duration = AK::Duration::from_milliseconds(static_cast<i64>(duration * 1000.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioThread::run()
|
|
||||||
{
|
|
||||||
auto devices = make<QMediaDevices>();
|
|
||||||
auto audio_device = AudioDevice::create(m_loader);
|
|
||||||
|
|
||||||
connect(devices, &QMediaDevices::audioOutputsChanged, this, [this]() {
|
|
||||||
queue_task({ AudioTask::Type::RecreateAudioDevice }).release_value_but_fixme_should_propagate_errors();
|
|
||||||
});
|
|
||||||
|
|
||||||
auto paused = Paused::Yes;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
auto& audio_output = audio_device.audio_output;
|
|
||||||
auto* io_device = audio_device.io_device;
|
|
||||||
|
|
||||||
if (auto result = m_task_queue.dequeue(); result.is_error()) {
|
|
||||||
VERIFY(result.error() == AudioTaskQueue::QueueStatus::Empty);
|
|
||||||
} else {
|
|
||||||
auto task = result.release_value();
|
|
||||||
|
|
||||||
switch (task.type) {
|
|
||||||
case AudioTask::Type::Stop:
|
|
||||||
return;
|
|
||||||
|
|
||||||
case AudioTask::Type::Play:
|
|
||||||
audio_output->resume();
|
|
||||||
paused = Paused::No;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AudioTask::Type::Pause:
|
|
||||||
audio_output->suspend();
|
|
||||||
paused = Paused::Yes;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AudioTask::Type::Seek:
|
|
||||||
VERIFY(task.data.has_value());
|
|
||||||
m_position = Web::Platform::AudioCodecPlugin::set_loader_position(m_loader, *task.data, m_duration);
|
|
||||||
|
|
||||||
if (paused == Paused::Yes)
|
|
||||||
Q_EMIT playback_position_updated(m_position);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AudioTask::Type::Volume:
|
|
||||||
VERIFY(task.data.has_value());
|
|
||||||
audio_output->setVolume(*task.data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AudioTask::Type::RecreateAudioDevice:
|
|
||||||
audio_device = AudioDevice::create(m_loader);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paused == Paused::No) {
|
|
||||||
if (auto result = play_next_samples(*audio_output, *io_device); result.is_error()) {
|
|
||||||
// FIXME: Propagate the error to the HTMLMediaElement.
|
|
||||||
} else {
|
|
||||||
Q_EMIT playback_position_updated(m_position);
|
|
||||||
paused = result.value();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usleep(UPDATE_RATE_MS * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<AudioThread::Paused> AudioThread::play_next_samples(QAudioSink& audio_output, QIODevice& io_device)
|
|
||||||
{
|
|
||||||
bool all_samples_loaded = m_loader->loaded_samples() >= m_loader->total_samples();
|
|
||||||
|
|
||||||
if (all_samples_loaded) {
|
|
||||||
audio_output.suspend();
|
|
||||||
(void)m_loader->reset();
|
|
||||||
|
|
||||||
m_position = m_duration;
|
|
||||||
return Paused::Yes;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto bytes_available = audio_output.bytesFree();
|
|
||||||
auto bytes_per_sample = audio_output.format().bytesPerSample();
|
|
||||||
auto channel_count = audio_output.format().channelCount();
|
|
||||||
auto samples_to_load = bytes_available / bytes_per_sample / channel_count;
|
|
||||||
|
|
||||||
auto samples = TRY(Web::Platform::AudioCodecPlugin::read_samples_from_loader(*m_loader, samples_to_load));
|
|
||||||
enqueue_samples(audio_output, io_device, move(samples));
|
|
||||||
|
|
||||||
m_position = Web::Platform::AudioCodecPlugin::current_loader_position(m_loader);
|
|
||||||
return Paused::No;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioThread::enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray<Audio::Sample> samples)
|
|
||||||
{
|
|
||||||
auto buffer_size = samples.size() * audio_output.format().bytesPerSample() * audio_output.format().channelCount();
|
|
||||||
|
|
||||||
if (buffer_size > static_cast<size_t>(m_sample_buffer.size()))
|
|
||||||
m_sample_buffer.resize(buffer_size);
|
|
||||||
|
|
||||||
FixedMemoryStream stream { Bytes { m_sample_buffer.data(), buffer_size } };
|
|
||||||
|
|
||||||
for (auto const& sample : samples) {
|
|
||||||
switch (audio_output.format().sampleFormat()) {
|
|
||||||
case QAudioFormat::UInt8:
|
|
||||||
write_sample<u8>(stream, sample.left);
|
|
||||||
write_sample<u8>(stream, sample.right);
|
|
||||||
break;
|
|
||||||
case QAudioFormat::Int16:
|
|
||||||
write_sample<i16>(stream, sample.left);
|
|
||||||
write_sample<i16>(stream, sample.right);
|
|
||||||
break;
|
|
||||||
case QAudioFormat::Int32:
|
|
||||||
write_sample<i32>(stream, sample.left);
|
|
||||||
write_sample<i32>(stream, sample.right);
|
|
||||||
break;
|
|
||||||
case QAudioFormat::Float:
|
|
||||||
write_sample<float>(stream, sample.left);
|
|
||||||
write_sample<float>(stream, sample.right);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
io_device.write(m_sample_buffer.data(), buffer_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
|
||||||
* Copyright (c) 2023, Andrew Kaster <akaster@serenityos.org>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <AK/Endian.h>
|
|
||||||
#include <AK/MemoryStream.h>
|
|
||||||
#include <AK/Optional.h>
|
|
||||||
#include <AK/Time.h>
|
|
||||||
#include <AK/Types.h>
|
|
||||||
#include <LibCore/SharedCircularQueue.h>
|
|
||||||
#include <LibMedia/Audio/Loader.h>
|
|
||||||
#include <LibMedia/Audio/Sample.h>
|
|
||||||
#include <QAudioFormat>
|
|
||||||
#include <QAudioSink>
|
|
||||||
#include <QByteArray>
|
|
||||||
#include <QMediaDevices>
|
|
||||||
#include <QThread>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
static constexpr u32 UPDATE_RATE_MS = 10;
|
|
||||||
|
|
||||||
struct AudioTask {
|
|
||||||
enum class Type {
|
|
||||||
Stop,
|
|
||||||
Play,
|
|
||||||
Pause,
|
|
||||||
Seek,
|
|
||||||
Volume,
|
|
||||||
RecreateAudioDevice,
|
|
||||||
};
|
|
||||||
|
|
||||||
Type type;
|
|
||||||
Optional<double> data {};
|
|
||||||
};
|
|
||||||
|
|
||||||
using AudioTaskQueue = Core::SharedSingleProducerCircularQueue<AudioTask>;
|
|
||||||
|
|
||||||
class AudioThread final : public QThread { // We have to use QThread, otherwise internal Qt media QTimer objects do not work.
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
static ErrorOr<NonnullOwnPtr<AudioThread>> create(NonnullRefPtr<Audio::Loader> loader);
|
|
||||||
|
|
||||||
ErrorOr<void> stop();
|
|
||||||
|
|
||||||
AK::Duration duration() const { return m_duration; }
|
|
||||||
|
|
||||||
ErrorOr<void> queue_task(AudioTask task);
|
|
||||||
|
|
||||||
Q_SIGNALS:
|
|
||||||
void playback_position_updated(AK::Duration);
|
|
||||||
|
|
||||||
private:
|
|
||||||
AudioThread(NonnullRefPtr<Audio::Loader> loader, AudioTaskQueue task_queue);
|
|
||||||
|
|
||||||
enum class Paused {
|
|
||||||
Yes,
|
|
||||||
No,
|
|
||||||
};
|
|
||||||
|
|
||||||
void run() override;
|
|
||||||
|
|
||||||
ErrorOr<Paused> play_next_samples(QAudioSink& audio_output, QIODevice& io_device);
|
|
||||||
|
|
||||||
void enqueue_samples(QAudioSink const& audio_output, QIODevice& io_device, FixedArray<Audio::Sample> samples);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
void write_sample(FixedMemoryStream& stream, float sample)
|
|
||||||
{
|
|
||||||
// The values that need to be written to the stream vary depending on the output channel format, and isn't
|
|
||||||
// particularly well documented. The value derivations performed below were adapted from a Qt example:
|
|
||||||
// https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/audiooutput/audiooutput.cpp?h=6.4.2#n46
|
|
||||||
LittleEndian<T> pcm;
|
|
||||||
|
|
||||||
if constexpr (IsSame<T, u8>)
|
|
||||||
pcm = static_cast<u8>((sample + 1.0f) / 2 * NumericLimits<u8>::max());
|
|
||||||
else if constexpr (IsSame<T, i16>)
|
|
||||||
pcm = static_cast<i16>(sample * NumericLimits<i16>::max());
|
|
||||||
else if constexpr (IsSame<T, i32>)
|
|
||||||
pcm = static_cast<i32>(sample * NumericLimits<i32>::max());
|
|
||||||
else if constexpr (IsSame<T, float>)
|
|
||||||
pcm = sample;
|
|
||||||
else
|
|
||||||
static_assert(DependentFalse<T>);
|
|
||||||
|
|
||||||
MUST(stream.write_value(pcm));
|
|
||||||
}
|
|
||||||
|
|
||||||
NonnullRefPtr<Audio::Loader> m_loader;
|
|
||||||
AudioTaskQueue m_task_queue;
|
|
||||||
|
|
||||||
QByteArray m_sample_buffer;
|
|
||||||
|
|
||||||
AK::Duration m_duration;
|
|
||||||
AK::Duration m_position;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,159 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "AutoComplete.h"
|
|
||||||
#include "Settings.h"
|
|
||||||
#include <AK/JsonArray.h>
|
|
||||||
#include <AK/JsonObject.h>
|
|
||||||
#include <AK/JsonParser.h>
|
|
||||||
#include <LibURL/URL.h>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
AutoComplete::AutoComplete(QWidget* parent)
|
|
||||||
: QCompleter(parent)
|
|
||||||
{
|
|
||||||
m_tree_view = new QTreeView(parent);
|
|
||||||
m_manager = new QNetworkAccessManager(this);
|
|
||||||
m_auto_complete_model = new AutoCompleteModel(this);
|
|
||||||
|
|
||||||
setCompletionMode(QCompleter::UnfilteredPopupCompletion);
|
|
||||||
setModel(m_auto_complete_model);
|
|
||||||
setPopup(m_tree_view);
|
|
||||||
|
|
||||||
m_tree_view->setRootIsDecorated(false);
|
|
||||||
m_tree_view->setHeaderHidden(true);
|
|
||||||
|
|
||||||
connect(this, QOverload<QModelIndex const&>::of(&QCompleter::activated), this, [&](QModelIndex const& index) {
|
|
||||||
emit activated(index);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(m_manager, &QNetworkAccessManager::finished, this, [&](QNetworkReply* reply) {
|
|
||||||
auto result = got_network_response(reply);
|
|
||||||
if (result.is_error())
|
|
||||||
dbgln("AutoComplete::got_network_response: Error {}", result.error());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<Vector<String>> AutoComplete::parse_google_autocomplete(Vector<JsonValue> const& json)
|
|
||||||
{
|
|
||||||
if (json.size() != 5)
|
|
||||||
return Error::from_string_literal("Invalid JSON, expected 5 elements in array");
|
|
||||||
|
|
||||||
if (!json[0].is_string())
|
|
||||||
return Error::from_string_literal("Invalid JSON, expected first element to be a string");
|
|
||||||
auto query = TRY(String::from_byte_string(json[0].as_string()));
|
|
||||||
|
|
||||||
if (!json[1].is_array())
|
|
||||||
return Error::from_string_literal("Invalid JSON, expected second element to be an array");
|
|
||||||
auto suggestions_array = json[1].as_array().values();
|
|
||||||
|
|
||||||
if (query != m_query)
|
|
||||||
return Error::from_string_literal("Invalid JSON, query does not match");
|
|
||||||
|
|
||||||
Vector<String> results;
|
|
||||||
results.ensure_capacity(suggestions_array.size());
|
|
||||||
for (auto& suggestion : suggestions_array)
|
|
||||||
results.unchecked_append(MUST(String::from_byte_string(suggestion.as_string())));
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<Vector<String>> AutoComplete::parse_duckduckgo_autocomplete(Vector<JsonValue> const& json)
|
|
||||||
{
|
|
||||||
Vector<String> results;
|
|
||||||
for (auto const& suggestion : json) {
|
|
||||||
auto maybe_value = suggestion.as_object().get("phrase"sv);
|
|
||||||
if (!maybe_value.has_value())
|
|
||||||
continue;
|
|
||||||
results.append(MUST(String::from_byte_string(maybe_value->as_string())));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<Vector<String>> AutoComplete::parse_yahoo_autocomplete(JsonObject const& json)
|
|
||||||
{
|
|
||||||
if (!json.get("q"sv).has_value() || !json.get("q"sv)->is_string())
|
|
||||||
return Error::from_string_view("Invalid JSON, expected \"q\" to be a string"sv);
|
|
||||||
auto query = TRY(String::from_byte_string(json.get("q"sv)->as_string()));
|
|
||||||
|
|
||||||
if (!json.get("r"sv).has_value() || !json.get("r"sv)->is_array())
|
|
||||||
return Error::from_string_view("Invalid JSON, expected \"r\" to be an object"sv);
|
|
||||||
auto suggestions_object = json.get("r"sv)->as_array().values();
|
|
||||||
|
|
||||||
if (query != m_query)
|
|
||||||
return Error::from_string_literal("Invalid JSON, query does not match");
|
|
||||||
|
|
||||||
Vector<String> results;
|
|
||||||
results.ensure_capacity(suggestions_object.size());
|
|
||||||
for (auto& suggestion_object : suggestions_object) {
|
|
||||||
if (!suggestion_object.is_object())
|
|
||||||
return Error::from_string_literal("Invalid JSON, expected value to be an object");
|
|
||||||
auto suggestion = suggestion_object.as_object();
|
|
||||||
|
|
||||||
if (!suggestion.get("k"sv).has_value() || !suggestion.get("k"sv)->is_string())
|
|
||||||
return Error::from_string_view("Invalid JSON, expected \"k\" to be a string"sv);
|
|
||||||
|
|
||||||
results.unchecked_append(MUST(String::from_byte_string(suggestion.get("k"sv)->as_string())));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorOr<void> AutoComplete::got_network_response(QNetworkReply* reply)
|
|
||||||
{
|
|
||||||
if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
AK::JsonParser parser(ak_byte_string_from_qstring(reply->readAll()));
|
|
||||||
auto json = TRY(parser.parse());
|
|
||||||
|
|
||||||
auto engine_name = Settings::the()->autocomplete_engine().name;
|
|
||||||
Vector<String> results;
|
|
||||||
if (engine_name == "Google") {
|
|
||||||
results = TRY(parse_google_autocomplete(json.as_array().values()));
|
|
||||||
} else if (engine_name == "DuckDuckGo") {
|
|
||||||
results = TRY(parse_duckduckgo_autocomplete(json.as_array().values()));
|
|
||||||
} else if (engine_name == "Yahoo")
|
|
||||||
results = TRY(parse_yahoo_autocomplete(json.as_object()));
|
|
||||||
else {
|
|
||||||
return Error::from_string_literal("Invalid engine name");
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr size_t MAX_AUTOCOMPLETE_RESULTS = 6;
|
|
||||||
if (results.is_empty()) {
|
|
||||||
results.append(m_query);
|
|
||||||
} else if (results.size() > MAX_AUTOCOMPLETE_RESULTS) {
|
|
||||||
results.shrink(MAX_AUTOCOMPLETE_RESULTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_auto_complete_model->replace_suggestions(move(results));
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
String AutoComplete::auto_complete_url_from_query(StringView query)
|
|
||||||
{
|
|
||||||
auto autocomplete_engine = ak_string_from_qstring(Settings::the()->autocomplete_engine().url);
|
|
||||||
return MUST(autocomplete_engine.replace("{}"sv, URL::percent_encode(query), ReplaceMode::FirstOnly));
|
|
||||||
}
|
|
||||||
|
|
||||||
void AutoComplete::clear_suggestions()
|
|
||||||
{
|
|
||||||
m_auto_complete_model->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void AutoComplete::get_search_suggestions(String search_string)
|
|
||||||
{
|
|
||||||
m_query = move(search_string);
|
|
||||||
if (m_reply)
|
|
||||||
m_reply->abort();
|
|
||||||
|
|
||||||
QNetworkRequest request { QUrl(qstring_from_ak_string(auto_complete_url_from_query(m_query))) };
|
|
||||||
m_reply = m_manager->get(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023, Cameron Youell <cameronyouell@gmail.com>
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "StringUtils.h"
|
|
||||||
#include <AK/Forward.h>
|
|
||||||
#include <AK/String.h>
|
|
||||||
#include <QCompleter>
|
|
||||||
#include <QNetworkReply>
|
|
||||||
#include <QTreeView>
|
|
||||||
|
|
||||||
namespace Ladybird {
|
|
||||||
|
|
||||||
class AutoCompleteModel final : public QAbstractListModel {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit AutoCompleteModel(QObject* parent)
|
|
||||||
: QAbstractListModel(parent)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual int rowCount(QModelIndex const& parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_suggestions.size(); }
|
|
||||||
virtual QVariant data(QModelIndex const& index, int role = Qt::DisplayRole) const override
|
|
||||||
{
|
|
||||||
if (role == Qt::DisplayRole || role == Qt::EditRole)
|
|
||||||
return qstring_from_ak_string(m_suggestions[index.row()]);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(String const& result)
|
|
||||||
{
|
|
||||||
beginInsertRows({}, m_suggestions.size(), m_suggestions.size());
|
|
||||||
m_suggestions.append(result);
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear()
|
|
||||||
{
|
|
||||||
beginResetModel();
|
|
||||||
m_suggestions.clear();
|
|
||||||
endResetModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
void replace_suggestions(Vector<String> suggestions)
|
|
||||||
{
|
|
||||||
beginInsertRows({}, m_suggestions.size(), m_suggestions.size());
|
|
||||||
m_suggestions = suggestions;
|
|
||||||
endInsertRows();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
AK::Vector<String> m_suggestions;
|
|
||||||
};
|
|
||||||
|
|
||||||
class AutoComplete final : public QCompleter {
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
AutoComplete(QWidget* parent);
|
|
||||||
|
|
||||||
virtual QString pathFromIndex(QModelIndex const& index) const override
|
|
||||||
{
|
|
||||||
return index.data(Qt::DisplayRole).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void get_search_suggestions(String);
|
|
||||||
void clear_suggestions();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void activated(QModelIndex const&);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static String auto_complete_url_from_query(StringView query);
|
|
||||||
|
|
||||||
ErrorOr<void> got_network_response(QNetworkReply* reply);
|
|
||||||
|
|
||||||
ErrorOr<Vector<String>> parse_google_autocomplete(Vector<JsonValue> const&);
|
|
||||||
ErrorOr<Vector<String>> parse_duckduckgo_autocomplete(Vector<JsonValue> const&);
|
|
||||||
ErrorOr<Vector<String>> parse_yahoo_autocomplete(JsonObject const&);
|
|
||||||
|
|
||||||
QNetworkAccessManager* m_manager;
|
|
||||||
AutoCompleteModel* m_auto_complete_model;
|
|
||||||
QTreeView* m_tree_view;
|
|
||||||
QNetworkReply* m_reply { nullptr };
|
|
||||||
|
|
||||||
String m_query;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue