30 Commits

Author SHA1 Message Date
ayeteadoe
fa262d2db5 LibCore: Add VERIFY checks for all wait completion packet nt.dll calls
Some checks failed
Nightly Lagom / Linux, arm64, Distribution, Clang (push) Has been cancelled
Nightly Lagom / macOS, arm64, Distribution, Clang (push) Has been cancelled
Nightly Lagom / Linux, arm64, Sanitizer, Clang (push) Has been cancelled
Nightly Lagom / macOS, arm64, Sanitizer, Swift (push) Has been cancelled
Nightly Lagom / Linux, x86_64, Distribution, GNU (push) Has been cancelled
Nightly Lagom / Linux, x86_64, Sanitizer, Swift (push) Has been cancelled
Nightly Lagom / Windows, x86_64, Windows_Sanitizer_CI, ClangCL (push) Has been cancelled
Nightly Lagom / Flatpak aarch64 (push) Has been cancelled
Nightly Lagom / Flatpak x86_64 (push) Has been cancelled
Close stale PRs / stale (push) Has been cancelled
Package the js repl as a binary artifact / Linux, arm64 (push) Has been cancelled
Package the js repl as a binary artifact / macOS, arm64 (push) Has been cancelled
Package the js repl as a binary artifact / Linux, x86_64 (push) Has been cancelled
Label PRs with merge conflicts / auto-labeler (push) Has been cancelled
2025-11-18 18:49:37 +01:00
ayeteadoe
d5e5dbdf3d LibCore: Signal an event to queue a wake completion packet on Windows
The initial IOCP event loop implementation adjusted wake() to manually
queue a completion packet onto the current threads IOCP. This caused
us to now be dependent on the current threads IOCP, when the previous
behaviour did not depend on any data from the thread that was waking
the event loop.

Restoring that old behaviour allows https://hardwaretester.com/gamepad
to be loaded again.
2025-11-18 18:49:37 +01:00
ayeteadoe
540bbae480 LibCore: Restore single-shot timer objects manual reset on Windows
The initial IOCP event loop implementation removed the single shot
timer fix added in 0005207 was removed.

Adding this back allowed simple web pages like https://ladybird.org/ to
be loaded again.
2025-11-18 18:49:37 +01:00
ayeteadoe
11b8bbeadf LibCore: Use correct fd for NotifierActivationEvent on Windows
The initial IOCP event loop implementation had a fd() method for the
EventLoopNotifier packet that did not actually return the fd for the
notifier, but a to_fd() call on an object HANDLE that was always NULL.
This meant we were always posting NotifierActivationEvents with a fd of
0.

This rendered all of our WinSock2 I/O invalid, meaning no IPC messages
would ever be successfully sent or received.
2025-11-18 18:49:37 +01:00
Sam Atkins
7d2f631d4c LibWeb/CSS: Handle whitespace better in font-language-override strings
The rules for strings here are:
- 4 ASCII characters long
- Shorter ones are right-padded with spaces before use
- Trailing whitespace is always removed when serializing

We previously always padded them during parsing, which was incorrect.
This commit flips it around so we trim trailing whitespace when parsing.

We don't yet actually use this property's value for anything. Once we do
so, maybe we'll care more about them being stored as 4 characters
always, but for now this avoids us needing a special step during
computation.
2025-11-18 17:22:03 +01:00
Jelle Raaijmakers
61f9b324c7 LibWeb: Always serialize CSSScale with at least two values 2025-11-18 14:44:49 +00:00
Jelle Raaijmakers
3083b19b09 LibWeb: Show bounding_rect in PushStackingContext dump 2025-11-18 15:10:58 +01:00
Jelle Raaijmakers
740629a3a8 LibWeb: Pass reference to bounding rect for Skia paint style 2025-11-18 15:10:58 +01:00
Jelle Raaijmakers
ba40da5e5f LibWeb: Rename DisplayList's apply_filters to apply_filter 2025-11-18 15:10:58 +01:00
Jelle Raaijmakers
d7ebc4eef4 LibWeb: Remove PaintableBox const_casts from ViewportPaintable
They're not necessary here - no functional changes.
2025-11-18 15:10:58 +01:00
Jelle Raaijmakers
9547889a6b LibWeb: Remove unused include from StackingContext 2025-11-18 15:10:58 +01:00
Jelle Raaijmakers
871f121c75 Everywhere: Validate // NOTE: ... and // NB: ... comments 2025-11-18 09:07:37 -05:00
Jelle Raaijmakers
8a02161481 Documentation: Standardize our spec note comment prefixes 2025-11-18 09:07:37 -05:00
Jelle Raaijmakers
41eb7251e4 LibWeb: Convert Ladybird notes in spec steps to // NB: ...
We have a couple of ways to designate spec notes and (our) developer
notes in comments, but we never really settled on a single approach. As
a result, we have a bit of a mixed bag of note comments on our hands.

To the extent that I could find them, I changed developer notes to
`// NB: ...` and changed spec notes to `// NOTE: ...`. The rationale for
this is that in most web specs, notes are prefixed by `NOTE: ...` so
this makes it easier to copy paste verbatim. The choice for `NB: ...` is
pretty arbitrary, but it makes it stand out from the regular spec notes
and it was already in wide use in our codebase.
2025-11-18 09:07:37 -05:00
Jelle Raaijmakers
9394c9a10b LibWeb: Deduplicate transformation creation logic
We had two code blocks responsible for turning a
TransformationStyleValue into a Transformation; get rid of one.
2025-11-18 14:36:26 +01:00
Jelle Raaijmakers
15fa6676b0 LibWeb: Simplify generation of perspective() matrix transform
A value of `perspective(none)` should result in the identity matrix. But
instead of creating that identity matrix explicitly, just break and
default to the identity matrix at the bottom of this method.
2025-11-18 14:36:26 +01:00
Jelle Raaijmakers
e4dc2663ba LibWeb: Reimplement transform interpolation according to spec
We had a partial implementation of transformation function interpolation
that did not support numerical interpolation of simple functions (e.g.
`scale(0)` -> `scale(1)`). This refactors the interpolation to follow
the spec more closely.

Gains us 267 WPT subtest passes in `css/css-transforms`.

Fixes #6774.
2025-11-18 14:36:26 +01:00
Timothy Flynn
911ecf1450 AK: Avoid copying the iterable container in AK::enumerate
There are actually a couple of issues here:

1. We are not properly perfect-forwarding the iterable to the Enumerator
   member. We are using the class template as the constructor type, but
   we would actually have to do something like this to achieve perfect
   forwarding:

   template <typname Iter = Iterable>
   Enumerator(Iter&&)

2. The begin / end methods on Enumerator (although they return by const-
   ref) are making copies during for-each loops. The compiler basically
   generates this when we call enumerate:

   for (auto it = Enumerator::begin(); it != Enumerator::end(); ++it)

   The creation of `it` above actually creates a copy of the returned
   Enumerator instance.

To avoid all of this, let's create an intermediate structure to act as
the enumerated iterator. This structure does not hold the iterable and
thus is fine to copy. We can then let the compiler handle forwarding
the iterable to the Enumerator.

Cherry-picked from:
0edcd19615
2025-11-18 13:31:52 +01:00
Aliaksandr Kalenik
195a67ed80 LibWeb: Delete dead code from Node::append_child() 2025-11-17 23:59:27 +01:00
Rocco Corsi
5fb78ae455 LibJS: Correct completion_cell typo in GeneratorObject 2025-11-17 23:43:04 +01:00
Andreas Kling
04238d0f3f LibJS: Make AsyncGenerator and GeneratorObject factories infallible
These should never fail.
2025-11-17 23:42:51 +01:00
Andreas Kling
d8f5971ddf LibJS: Allow constructing generator from function with null "prototype"
Fixes 4 test262 tests and simplifies some upcoming stuff.
2025-11-17 23:42:51 +01:00
Tim Ledbetter
acce880359 Tests: Disable naturalWidth-naturalHeight-width-height.tentative.html
This test is currently failing fairly frequently on CI.
2025-11-17 18:49:21 +00:00
Sam Atkins
1ae4429f3b LibWeb/CSS: Stop inserting whitespace in TokenStream::dump_string()
Another one I missed before!
2025-11-17 19:11:48 +01:00
Sam Atkins
eaece1d12c LibWeb/CSS: Stop inserting whitespace when serializing SimpleBlocks
I missed this in #6646. It mostly affects debug logging, but it's nice
when logs reflect reality.
2025-11-17 19:11:48 +01:00
Lorenz A
5dbb857c40 Tests/LibWeb: Prevent flake with iframe loading in gamepad-iframe.html
If `sendMessageAndWait` sends a message before the iframe is setup and
listening we would wait indefinitely making the test timeout.
2025-11-17 17:21:31 +01:00
Zaggy1024
0664e9fbf5 Tests: Ensure that AudioDataProvider never outputs overlapping blocks 2025-11-17 16:51:18 +01:00
Zaggy1024
ae5e200dfc LibMedia: Move overlapping audio block correction to the data provider
This prevents PlaybackManager's seek while enabling an audio track from
causing the AudioMixingSink to push audio blocks forward unnecessarily.
Previously, the seek would cause the initial block or blocks to repeat
from the perspective of AudioMixingSink, so it would think that it
needs to shift the first block after the seek forward by a few samples.
By moving this to the AudioDataProvider, we can clear the last sample
index every time the decoder is flushed, ensuring that the block
shifting always makes sense.

By doing this in AudioMixingSink instead of the Decoder
implementations, we avoid having to duplicate this shifting logic
across multiple implementations.

This also fixes an issue where multiple audio blocks occupying the same
timestamp would be skipped while seeking, causing a significant break
in audio.
2025-11-17 16:51:18 +01:00
Zaggy1024
7e325d64f5 LibMedia: Use [from/to]_time_units in FFmpegDemuxer and AudioMixingSink 2025-11-17 16:51:18 +01:00
Zaggy1024
a1358fa970 AK+Tests: Add time units conversion functions to Duration
These take a numerator and denominator defining the unit in a fractions
of a second. Conversion is done in integers, meaning that these must
clamp when approaching numeric limits.
2025-11-17 16:51:18 +01:00
57 changed files with 982 additions and 334 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2024-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -13,7 +13,7 @@ namespace AK {
namespace Detail {
template<typename Iterable>
class Enumerator {
struct Enumerator {
using IteratorType = decltype(declval<Iterable>().begin());
using ValueType = decltype(*declval<IteratorType>());
@@ -22,34 +22,26 @@ class Enumerator {
ValueType value;
};
public:
Enumerator(Iterable&& iterable)
: m_iterable(forward<Iterable>(iterable))
, m_iterator(m_iterable.begin())
, m_end(m_iterable.end())
{
}
struct Iterator {
Enumeration operator*() { return { index, *iterator }; }
Enumeration operator*() const { return { index, *iterator }; }
Enumerator const& begin() const { return *this; }
Enumerator const& end() const { return *this; }
bool operator!=(Iterator const& other) const { return iterator != other.iterator; }
Enumeration operator*() { return { m_index, *m_iterator }; }
Enumeration operator*() const { return { m_index, *m_iterator }; }
void operator++()
{
++index;
++iterator;
}
bool operator!=(Enumerator const&) const { return m_iterator != m_end; }
size_t index { 0 };
IteratorType iterator;
};
void operator++()
{
++m_index;
++m_iterator;
}
Iterator begin() { return { 0, iterable.begin() }; }
Iterator end() { return { 0, iterable.end() }; }
private:
Iterable m_iterable;
size_t m_index { 0 };
IteratorType m_iterator;
IteratorType const m_end;
Iterable iterable;
};
}

View File

@@ -67,6 +67,28 @@ Duration Duration::from_timeval(const struct timeval& tv)
return Duration::from_half_sanitized(tv.tv_sec, extra_secs, usecs * 1'000);
}
Duration Duration::from_time_units(i64 time_units, u32 numerator, u32 denominator)
{
VERIFY(numerator != 0);
VERIFY(denominator != 0);
auto seconds_checked = Checked<i64>(time_units);
seconds_checked.mul(numerator);
seconds_checked.div(denominator);
if (time_units < 0)
seconds_checked.sub(1);
if (seconds_checked.has_overflow())
return Duration(time_units >= 0 ? NumericLimits<i64>::max() : NumericLimits<i64>::min(), 0);
auto seconds = seconds_checked.value_unchecked();
auto seconds_in_time_units = seconds * denominator / numerator;
auto remainder_in_time_units = time_units - seconds_in_time_units;
auto nanoseconds = ((remainder_in_time_units * 1'000'000'000 * numerator) + (denominator / 2)) / denominator;
VERIFY(nanoseconds >= 0);
VERIFY(nanoseconds < 1'000'000'000);
return Duration(seconds, static_cast<u32>(nanoseconds));
}
i64 Duration::to_truncated_seconds() const
{
VERIFY(m_nanoseconds < 1'000'000'000);
@@ -196,6 +218,22 @@ timeval Duration::to_timeval() const
return { static_cast<sec_type>(m_seconds), static_cast<usec_type>(m_nanoseconds) / 1000 };
}
i64 Duration::to_time_units(u32 numerator, u32 denominator) const
{
VERIFY(numerator != 0);
VERIFY(denominator != 0);
auto seconds_product = Checked<i64>::saturating_mul(m_seconds, denominator);
auto time_units = seconds_product / numerator;
auto remainder = seconds_product % numerator;
auto remainder_in_nanoseconds = remainder * 1'000'000'000;
auto rounding_half = static_cast<i64>(numerator) * 500'000'000;
time_units = Checked<i64>::saturating_add(time_units, ((static_cast<i64>(m_nanoseconds) * denominator + remainder_in_nanoseconds + rounding_half) / numerator) / 1'000'000'000);
return time_units;
}
Duration Duration::from_half_sanitized(i64 seconds, i32 extra_seconds, u32 nanoseconds)
{
VERIFY(nanoseconds < 1'000'000'000);

View File

@@ -245,6 +245,7 @@ public:
[[nodiscard]] static Duration from_ticks(clock_t, time_t);
[[nodiscard]] static Duration from_timespec(const struct timespec&);
[[nodiscard]] static Duration from_timeval(const struct timeval&);
[[nodiscard]] static Duration from_time_units(i64 units, u32 numerator, u32 denominator);
// We don't pull in <stdint.h> for the pretty min/max definitions because this file is also included in the Kernel
[[nodiscard]] constexpr static Duration min() { return Duration(-__INT64_MAX__ - 1LL, 0); }
[[nodiscard]] constexpr static Duration zero() { return Duration(0, 0); }
@@ -263,6 +264,7 @@ public:
[[nodiscard]] timespec to_timespec() const;
// Rounds towards -inf (it was the easiest to implement).
[[nodiscard]] timeval to_timeval() const;
[[nodiscard]] i64 to_time_units(u32 numerator, u32 denominator) const;
[[nodiscard]] bool is_zero() const { return (m_seconds == 0) && (m_nanoseconds == 0); }
[[nodiscard]] bool is_negative() const { return m_seconds < 0; }

View File

@@ -589,6 +589,31 @@ catdog_widget.on_click = [&] {
};
```
#### Spec notes
Many web specs include notes that are prefixed with `NOTE: ...`. To allow for verbatim copying of these notes into our
code, we retain the `NOTE:` prefix and use [`NB:`](https://en.wikipedia.org/wiki/Nota_bene) for our own notes.
This only applies to comments as part of code that is directly implementing a spec algorithm or behavior. Comments in
other places do not need a prefix.
##### Right:
```cpp
// 2. If property is in already serialized, continue with the steps labeled declaration loop.
// NOTE: The prefabulated aluminite will not be suitable for use here. If the listed spec note is so long that we reach
// column 120, we wrap around and indent the lines to match up with the first line.
// NB: We _can_ actually use the aluminite since we unprefabulated it in step 1 for performance reasons.
```
##### Wrong:
```cpp
// LB-NOTE: The aluminite might come pre-prefabulated at this point.
// Spec-note: Another example of a custom note prefix that we shouldn't use.
// There is no prefix whatsoever here, making it unclear whether this is a spec step, note or a developer note.
```
### Overriding Virtual Methods
The declaration of a virtual method inside a class must be declared with the `virtual` keyword. All subclasses of that class must also specify either the `override` keyword when overriding the virtual method, or the `final` keyword when overriding the virtual method and requiring that no further subclasses can override it.

View File

@@ -72,6 +72,11 @@ struct CompletionPacket {
CompletionType type;
};
struct EventLoopWake final : CompletionPacket {
OwnHandle wait_packet;
OwnHandle wait_event;
};
struct EventLoopTimer final : CompletionPacket {
~EventLoopTimer()
@@ -92,12 +97,12 @@ struct EventLoopNotifier final : CompletionPacket {
}
Notifier::Type notifier_type() const { return m_notifier_type; }
int fd() const { return to_fd(object_handle); }
int notifier_fd() const { return m_notifier_fd; }
// These are a space tradeoff for avoiding a double indirection through the notifier*.
Notifier* notifier;
Notifier::Type m_notifier_type;
HANDLE object_handle;
int m_notifier_fd { -1 };
OwnHandle wait_packet;
OwnHandle wait_event;
};
@@ -112,11 +117,24 @@ struct ThreadData {
}
ThreadData()
: wake_completion_key(make<CompletionPacket>(CompletionType::Wake))
: wake_data(make<EventLoopWake>())
{
wake_data->type = CompletionType::Wake;
wake_data->wait_event.handle = CreateEvent(NULL, FALSE, FALSE, NULL);
// Consider a way for different event loops to have a different number of threads
iocp.handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);
VERIFY(iocp.handle);
NTSTATUS status = g_system.NtCreateWaitCompletionPacket(&wake_data->wait_packet.handle, GENERIC_READ | GENERIC_WRITE, NULL);
VERIFY(NT_SUCCESS(status));
status = g_system.NtAssociateWaitCompletionPacket(wake_data->wait_packet.handle, iocp.handle, wake_data->wait_event.handle, wake_data.ptr(), NULL, 0, 0, NULL);
VERIFY(NT_SUCCESS(status));
}
~ThreadData()
{
NTSTATUS status = g_system.NtCancelWaitCompletionPacket(wake_data->wait_packet.handle, TRUE);
VERIFY(NT_SUCCESS(status));
}
OwnHandle iocp;
@@ -125,12 +143,12 @@ struct ThreadData {
HashMap<intptr_t, NonnullOwnPtr<EventLoopTimer>> timers;
HashMap<Notifier*, NonnullOwnPtr<EventLoopNotifier>> notifiers;
// The wake completion key is posted to the thread's event loop to wake it.
NonnullOwnPtr<CompletionPacket> wake_completion_key;
// The wake completion packet is posted to the thread's event loop to wake it.
NonnullOwnPtr<EventLoopWake> wake_data;
};
EventLoopImplementationWindows::EventLoopImplementationWindows()
: m_wake_completion_key((void*)ThreadData::the()->wake_completion_key.ptr())
: m_wake_event(ThreadData::the()->wake_data->wait_event.handle)
{
}
@@ -175,21 +193,27 @@ size_t EventLoopImplementationWindows::pump(PumpMode pump_mode)
auto& entry = entries[i];
auto* packet = reinterpret_cast<CompletionPacket*>(entry.lpCompletionKey);
if (packet == thread_data->wake_completion_key) {
if (packet->type == CompletionType::Wake) {
auto* wake_data = static_cast<EventLoopWake*>(packet);
NTSTATUS status = g_system.NtAssociateWaitCompletionPacket(wake_data->wait_packet.handle, thread_data->iocp.handle, wake_data->wait_event.handle, wake_data, NULL, 0, 0, NULL);
VERIFY(NT_SUCCESS(status));
continue;
}
if (packet->type == CompletionType::Timer) {
auto* timer = static_cast<EventLoopTimer*>(packet);
if (auto owner = timer->owner.strong_ref())
event_queue.post_event(*owner, make<TimerEvent>());
if (timer->is_periodic)
g_system.NtAssociateWaitCompletionPacket(timer->wait_packet.handle, thread_data->iocp.handle, timer->timer.handle, timer, NULL, 0, 0, NULL);
if (timer->is_periodic) {
NTSTATUS status = g_system.NtAssociateWaitCompletionPacket(timer->wait_packet.handle, thread_data->iocp.handle, timer->timer.handle, timer, NULL, 0, 0, NULL);
VERIFY(NT_SUCCESS(status));
}
continue;
}
if (packet->type == CompletionType::Notifer) {
auto* notifier_data = reinterpret_cast<EventLoopNotifier*>(packet);
event_queue.post_event(*notifier_data->notifier, make<NotifierActivationEvent>(notifier_data->fd(), notifier_data->notifier_type()));
g_system.NtAssociateWaitCompletionPacket(notifier_data->wait_packet.handle, thread_data->iocp.handle, notifier_data->wait_event.handle, notifier_data, NULL, 0, 0, NULL);
auto* notifier_data = static_cast<EventLoopNotifier*>(packet);
event_queue.post_event(*notifier_data->notifier, make<NotifierActivationEvent>(notifier_data->notifier_fd(), notifier_data->notifier_type()));
NTSTATUS status = g_system.NtAssociateWaitCompletionPacket(notifier_data->wait_packet.handle, thread_data->iocp.handle, notifier_data->wait_event.handle, notifier_data, NULL, 0, 0, NULL);
VERIFY(NT_SUCCESS(status));
continue;
}
VERIFY_NOT_REACHED();
@@ -223,8 +247,7 @@ void EventLoopImplementationWindows::post_event(EventReceiver& receiver, Nonnull
void EventLoopImplementationWindows::wake()
{
auto* thread_data = ThreadData::the();
PostQueuedCompletionStatus(thread_data->iocp.handle, 0, (ULONG_PTR)m_wake_completion_key, NULL);
SetEvent(m_wake_event);
}
static int notifier_type_to_network_event(NotificationType type)
@@ -258,9 +281,9 @@ void EventLoopManagerWindows::register_notifier(Notifier& notifier)
notifier_data->notifier = &notifier;
notifier_data->m_notifier_type = notifier.type();
notifier_data->wait_event.handle = event;
NTSTATUS status = NtCreateWaitCompletionPacket(&notifier_data->wait_packet.handle, GENERIC_READ | GENERIC_WRITE, NULL);
NTSTATUS status = g_system.NtCreateWaitCompletionPacket(&notifier_data->wait_packet.handle, GENERIC_READ | GENERIC_WRITE, NULL);
VERIFY(NT_SUCCESS(status));
status = NtAssociateWaitCompletionPacket(notifier_data->wait_packet.handle, thread_data->iocp.handle, event, notifier_data.ptr(), NULL, 0, 0, NULL);
status = g_system.NtAssociateWaitCompletionPacket(notifier_data->wait_packet.handle, thread_data->iocp.handle, event, notifier_data.ptr(), NULL, 0, 0, NULL);
VERIFY(NT_SUCCESS(status));
notifiers.set(&notifier, move(notifier_data));
}
@@ -288,9 +311,14 @@ intptr_t EventLoopManagerWindows::register_timer(EventReceiver& object, int mill
VERIFY(thread_data);
auto& timers = thread_data->timers;
// FIXME: This is a temporary fix for issue #3641
bool manual_reset = static_cast<Timer&>(object).is_single_shot();
HANDLE timer = CreateWaitableTimer(NULL, manual_reset, NULL);
VERIFY(timer);
auto timer_data = make<EventLoopTimer>();
timer_data->type = CompletionType::Timer;
timer_data->timer.handle = CreateWaitableTimer(NULL, FALSE, NULL);
timer_data->timer.handle = timer;
timer_data->owner = object.make_weak_ptr();
timer_data->is_periodic = should_reload;
VERIFY(timer_data->timer.handle);
@@ -320,7 +348,8 @@ void EventLoopManagerWindows::unregister_timer(intptr_t timer_id)
if (!maybe_timer.has_value())
return;
auto timer = move(maybe_timer.value());
g_system.NtCancelWaitCompletionPacket(timer->wait_packet.handle, TRUE);
NTSTATUS status = g_system.NtCancelWaitCompletionPacket(timer->wait_packet.handle, TRUE);
VERIFY(NT_SUCCESS(status));
}
}

View File

@@ -48,7 +48,8 @@ private:
bool m_exit_requested { false };
int m_exit_code { 0 };
void const* m_wake_completion_key;
// The wake event handle of this event loop needs to be accessible from other threads.
void*& m_wake_event;
};
using EventLoopManagerPlatform = EventLoopManagerWindows;

View File

@@ -18,21 +18,23 @@ namespace JS {
GC_DEFINE_ALLOCATOR(AsyncGenerator);
ThrowCompletionOr<GC::Ref<AsyncGenerator>> AsyncGenerator::create(Realm& realm, Value initial_value, ECMAScriptFunctionObject* generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
GC::Ref<AsyncGenerator> AsyncGenerator::create(Realm& realm, Value initial_value, ECMAScriptFunctionObject* generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
{
auto& vm = realm.vm();
// This is "g1.prototype" in figure-2 (https://tc39.es/ecma262/img/figure-2.png)
static Bytecode::PropertyLookupCache cache;
auto generating_function_prototype = TRY(generating_function->get(vm.names.prototype, cache));
auto generating_function_prototype_object = TRY(generating_function_prototype.to_object(vm));
auto generating_function_prototype = MUST(generating_function->get(vm.names.prototype, cache));
GC::Ptr<Object> generating_function_prototype_object = nullptr;
if (!generating_function_prototype.is_nullish())
generating_function_prototype_object = MUST(generating_function_prototype.to_object(vm));
auto object = realm.create<AsyncGenerator>(realm, generating_function_prototype_object, move(execution_context));
object->m_generating_function = generating_function;
object->m_previous_value = initial_value;
return object;
}
AsyncGenerator::AsyncGenerator(Realm&, Object& prototype, NonnullOwnPtr<ExecutionContext> context)
: Object(ConstructWithPrototypeTag::Tag, prototype)
AsyncGenerator::AsyncGenerator(Realm& realm, Object* prototype, NonnullOwnPtr<ExecutionContext> context)
: Object(realm, prototype)
, m_async_generator_context(move(context))
{
}

View File

@@ -28,7 +28,7 @@ public:
Completed,
};
static ThrowCompletionOr<GC::Ref<AsyncGenerator>> create(Realm&, Value, ECMAScriptFunctionObject*, NonnullOwnPtr<ExecutionContext>);
static GC::Ref<AsyncGenerator> create(Realm&, Value, ECMAScriptFunctionObject*, NonnullOwnPtr<ExecutionContext>);
virtual ~AsyncGenerator() override;
@@ -44,7 +44,7 @@ public:
Optional<String> const& generator_brand() const { return m_generator_brand; }
private:
AsyncGenerator(Realm&, Object& prototype, NonnullOwnPtr<ExecutionContext>);
AsyncGenerator(Realm&, Object* prototype, NonnullOwnPtr<ExecutionContext>);
virtual void visit_edges(Cell::Visitor&) override;

View File

@@ -893,12 +893,10 @@ ThrowCompletionOr<Value> ECMAScriptFunctionObject::ordinary_call_evaluate_body(V
if (kind() == FunctionKind::Normal)
return result;
if (kind() == FunctionKind::AsyncGenerator) {
auto async_generator_object = TRY(AsyncGenerator::create(*context.realm, result, this, context.copy()));
return async_generator_object;
}
if (kind() == FunctionKind::AsyncGenerator)
return AsyncGenerator::create(*context.realm, result, this, context.copy());
auto generator_object = TRY(GeneratorObject::create(*context.realm, result, this, context.copy()));
auto generator_object = GeneratorObject::create(*context.realm, result, this, context.copy());
// NOTE: Async functions are entirely transformed to generator functions, and wrapped in a custom driver that returns a promise
// See AwaitExpression::generate_bytecode() for the transformation.

View File

@@ -18,7 +18,7 @@ namespace JS {
GC_DEFINE_ALLOCATOR(GeneratorObject);
ThrowCompletionOr<GC::Ref<GeneratorObject>> GeneratorObject::create(Realm& realm, Value initial_value, ECMAScriptFunctionObject* generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
GC::Ref<GeneratorObject> GeneratorObject::create(Realm& realm, Value initial_value, ECMAScriptFunctionObject* generating_function, NonnullOwnPtr<ExecutionContext> execution_context)
{
auto& vm = realm.vm();
// This is "g1.prototype" in figure-2 (https://tc39.es/ecma262/img/figure-2.png)
@@ -30,17 +30,19 @@ ThrowCompletionOr<GC::Ref<GeneratorObject>> GeneratorObject::create(Realm& realm
generating_function_prototype = realm.intrinsics().generator_prototype();
} else {
static Bytecode::PropertyLookupCache cache;
generating_function_prototype = TRY(generating_function->get(vm.names.prototype, cache));
generating_function_prototype = MUST(generating_function->get(vm.names.prototype, cache));
}
auto generating_function_prototype_object = TRY(generating_function_prototype.to_object(vm));
GC::Ptr<Object> generating_function_prototype_object = nullptr;
if (!generating_function_prototype.is_nullish())
generating_function_prototype_object = MUST(generating_function_prototype.to_object(vm));
auto object = realm.create<GeneratorObject>(realm, generating_function_prototype_object, move(execution_context));
object->m_generating_function = generating_function;
object->m_previous_value = initial_value;
return object;
}
GeneratorObject::GeneratorObject(Realm&, Object& prototype, NonnullOwnPtr<ExecutionContext> context, Optional<StringView> generator_brand)
: Object(ConstructWithPrototypeTag::Tag, prototype)
GeneratorObject::GeneratorObject(Realm& realm, Object* prototype, NonnullOwnPtr<ExecutionContext> context, Optional<StringView> generator_brand)
: Object(realm, prototype)
, m_execution_context(move(context))
, m_generator_brand(move(generator_brand))
{
@@ -99,7 +101,7 @@ ThrowCompletionOr<GeneratorObject::IterationResult> GeneratorObject::execute(VM&
return {};
};
auto compleion_cell = heap().allocate<CompletionCell>(completion);
auto completion_cell = heap().allocate<CompletionCell>(completion);
auto& bytecode_interpreter = vm.bytecode_interpreter();
@@ -108,7 +110,7 @@ ThrowCompletionOr<GeneratorObject::IterationResult> GeneratorObject::execute(VM&
// We should never enter `execute` again after the generator is complete.
VERIFY(next_block.has_value());
auto result_value = bytecode_interpreter.run_executable(vm.running_execution_context(), *m_generating_function->bytecode_executable(), next_block, compleion_cell);
auto result_value = bytecode_interpreter.run_executable(vm.running_execution_context(), *m_generating_function->bytecode_executable(), next_block, completion_cell);
vm.pop_execution_context();

View File

@@ -17,7 +17,7 @@ class GeneratorObject : public Object {
GC_DECLARE_ALLOCATOR(GeneratorObject);
public:
static ThrowCompletionOr<GC::Ref<GeneratorObject>> create(Realm&, Value, ECMAScriptFunctionObject*, NonnullOwnPtr<ExecutionContext>);
static GC::Ref<GeneratorObject> create(Realm&, Value, ECMAScriptFunctionObject*, NonnullOwnPtr<ExecutionContext>);
virtual ~GeneratorObject() override = default;
void visit_edges(Cell::Visitor&) override;
@@ -46,7 +46,7 @@ public:
void set_generator_state(GeneratorState generator_state) { m_generator_state = generator_state; }
protected:
GeneratorObject(Realm&, Object& prototype, NonnullOwnPtr<ExecutionContext>, Optional<StringView> generator_brand = {});
GeneratorObject(Realm&, Object* prototype, NonnullOwnPtr<ExecutionContext>, Optional<StringView> generator_brand = {});
ThrowCompletionOr<GeneratorState> validate(VM&, Optional<StringView> const& generator_brand);
virtual ThrowCompletionOr<IterationResult> execute(VM&, JS::Completion const& completion);

View File

@@ -19,7 +19,7 @@ ThrowCompletionOr<GC::Ref<IteratorHelper>> IteratorHelper::create(Realm& realm,
}
IteratorHelper::IteratorHelper(Realm& realm, Object& prototype, GC::Ref<IteratorRecord> underlying_iterator, GC::Ref<Closure> closure, GC::Ptr<AbruptClosure> abrupt_closure)
: GeneratorObject(realm, prototype, realm.vm().running_execution_context().copy(), "Iterator Helper"sv)
: GeneratorObject(realm, &prototype, realm.vm().running_execution_context().copy(), "Iterator Helper"sv)
, m_underlying_iterator(move(underlying_iterator))
, m_closure(closure)
, m_abrupt_closure(abrupt_closure)

View File

@@ -17,7 +17,8 @@ public:
u32 sample_rate() const { return m_sample_rate; }
u8 channel_count() const { return m_channel_count; }
AK::Duration start_timestamp() const { return m_start_timestamp; }
AK::Duration timestamp() const { return m_timestamp; }
i64 timestamp_in_samples() const { return m_timestamp_in_samples; }
Data& data() { return m_data; }
Data const& data() const { return m_data; }
@@ -25,20 +26,27 @@ public:
{
m_sample_rate = 0;
m_channel_count = 0;
m_start_timestamp = AK::Duration::zero();
m_timestamp_in_samples = 0;
m_data = Data();
}
template<typename Callback>
void emplace(u32 sample_rate, u8 channel_count, AK::Duration start_timestamp, Callback data_callback)
void emplace(u32 sample_rate, u8 channel_count, AK::Duration timestamp, Callback data_callback)
{
VERIFY(sample_rate != 0);
VERIFY(channel_count != 0);
VERIFY(m_data.is_empty());
m_sample_rate = sample_rate;
m_channel_count = channel_count;
m_start_timestamp = start_timestamp;
m_timestamp = timestamp;
m_timestamp_in_samples = timestamp.to_time_units(1, sample_rate);
data_callback(m_data);
}
void set_timestamp_in_samples(i64 timestamp_in_samples)
{
VERIFY(!is_empty());
m_timestamp_in_samples = timestamp_in_samples;
m_timestamp = AK::Duration::from_time_units(timestamp_in_samples, 1, m_sample_rate);
}
bool is_empty() const
{
return m_sample_rate == 0;
@@ -55,7 +63,8 @@ public:
private:
u32 m_sample_rate { 0 };
u8 m_channel_count { 0 };
AK::Duration m_start_timestamp;
AK::Duration m_timestamp;
i64 m_timestamp_in_samples { 0 };
Data m_data;
};

View File

@@ -75,37 +75,18 @@ FFmpegDemuxer::TrackContext& FFmpegDemuxer::get_track_context(Track const& track
});
}
static inline AK::Duration time_units_to_duration(i64 time_units, int numerator, int denominator)
{
VERIFY(numerator != 0);
VERIFY(denominator != 0);
auto seconds = time_units * numerator / denominator;
auto seconds_in_time_units = seconds * denominator / numerator;
auto remainder_in_time_units = time_units - seconds_in_time_units;
auto nanoseconds = ((remainder_in_time_units * 1'000'000'000 * numerator) + (denominator / 2)) / denominator;
return AK::Duration::from_seconds(seconds) + AK::Duration::from_nanoseconds(nanoseconds);
}
static inline AK::Duration time_units_to_duration(i64 time_units, AVRational const& time_base)
{
return time_units_to_duration(time_units, time_base.num, time_base.den);
}
static inline i64 duration_to_time_units(AK::Duration duration, int numerator, int denominator)
{
VERIFY(numerator != 0);
VERIFY(denominator != 0);
auto seconds = duration.to_truncated_seconds();
auto nanoseconds = (duration - AK::Duration::from_seconds(seconds)).to_nanoseconds();
auto time_units = seconds * denominator / numerator;
time_units += nanoseconds * denominator / numerator / 1'000'000'000;
return time_units;
VERIFY(time_base.num > 0);
VERIFY(time_base.den > 0);
return AK::Duration::from_time_units(time_units, time_base.num, time_base.den);
}
static inline i64 duration_to_time_units(AK::Duration duration, AVRational const& time_base)
{
return duration_to_time_units(duration, time_base.num, time_base.den);
VERIFY(time_base.num > 0);
VERIFY(time_base.den > 0);
return duration.to_time_units(time_base.num, time_base.den);
}
DecoderErrorOr<AK::Duration> FFmpegDemuxer::total_duration()
@@ -114,7 +95,7 @@ DecoderErrorOr<AK::Duration> FFmpegDemuxer::total_duration()
return DecoderError::format(DecoderErrorCategory::Unknown, "Negative stream duration");
}
return time_units_to_duration(m_format_context->duration, 1, AV_TIME_BASE);
return AK::Duration::from_time_units(m_format_context->duration, 1, AV_TIME_BASE);
}
DecoderErrorOr<AK::Duration> FFmpegDemuxer::duration_of_track(Track const& track)

View File

@@ -105,6 +105,21 @@ bool AudioDataProvider::ThreadData::should_thread_exit() const
return m_exit;
}
void AudioDataProvider::ThreadData::flush_decoder()
{
m_decoder->flush();
m_last_sample = NumericLimits<i64>::min();
}
DecoderErrorOr<void> AudioDataProvider::ThreadData::retrieve_next_block(AudioBlock& block)
{
TRY(m_decoder->write_next_block(block));
if (block.timestamp_in_samples() < m_last_sample)
block.set_timestamp_in_samples(m_last_sample);
m_last_sample = block.timestamp_in_samples() + static_cast<i64>(block.sample_count());
return {};
}
template<typename T>
void AudioDataProvider::ThreadData::process_seek_on_main_thread(u32 seek_id, T&& function)
{
@@ -166,7 +181,7 @@ bool AudioDataProvider::ThreadData::handle_seek()
auto demuxer_seek_result = demuxer_seek_result_or_error.value_or(DemuxerSeekResult::MovedPosition);
if (demuxer_seek_result == DemuxerSeekResult::MovedPosition)
m_decoder->flush();
flush_decoder();
auto new_seek_id = seek_id;
AudioBlock last_block;
@@ -191,7 +206,7 @@ bool AudioDataProvider::ThreadData::handle_seek()
while (new_seek_id == seek_id) {
AudioBlock current_block;
auto block_result = m_decoder->write_next_block(current_block);
auto block_result = retrieve_next_block(current_block);
if (block_result.is_error()) {
if (block_result.error().category() == DecoderErrorCategory::EndOfStream) {
resolve_seek(seek_id);
@@ -205,7 +220,7 @@ bool AudioDataProvider::ThreadData::handle_seek()
return true;
}
if (current_block.start_timestamp() > timestamp) {
if (current_block.timestamp() > timestamp) {
auto locker = take_lock();
m_queue.clear();
@@ -291,7 +306,7 @@ void AudioDataProvider::ThreadData::push_data_and_decode_a_block()
}
auto block = AudioBlock();
auto timestamp_result = m_decoder->write_next_block(block);
auto timestamp_result = retrieve_next_block(block);
if (timestamp_result.is_error()) {
if (timestamp_result.error().category() == DecoderErrorCategory::NeedsMoreInput)
break;

View File

@@ -53,6 +53,8 @@ private:
void set_error_handler(ErrorHandler&&);
bool should_thread_exit() const;
void flush_decoder();
DecoderErrorOr<void> retrieve_next_block(AudioBlock&);
bool handle_seek();
template<typename T>
void process_seek_on_main_thread(u32 seek_id, T&&);
@@ -80,6 +82,7 @@ private:
NonnullRefPtr<MutexedDemuxer> m_demuxer;
Track m_track;
NonnullOwnPtr<AudioDecoder> m_decoder;
i64 m_last_sample { NumericLimits<i64>::min() };
size_t m_queue_max_size { 8 };
AudioQueue m_queue;

View File

@@ -75,29 +75,6 @@ RefPtr<AudioDataProvider> AudioMixingSink::provider(Track const& track) const
return mixing_data->provider;
}
static inline i64 duration_to_sample(AK::Duration duration, u32 sample_rate)
{
VERIFY(sample_rate != 0);
auto seconds = duration.to_truncated_seconds();
auto nanoseconds = (duration - AK::Duration::from_seconds(seconds)).to_nanoseconds();
auto sample = seconds * sample_rate;
sample += nanoseconds * sample_rate / 1'000'000'000;
return sample;
}
static inline AK::Duration sample_to_duration(i64 sample, u32 sample_rate)
{
VERIFY(sample_rate != 0);
auto seconds = sample / sample_rate;
auto seconds_in_time_units = seconds * sample_rate;
auto remainder_in_time_units = sample - seconds_in_time_units;
auto nanoseconds = ((remainder_in_time_units * 1'000'000'000) + (sample_rate / 2)) / sample_rate;
return AK::Duration::from_seconds(seconds) + AK::Duration::from_nanoseconds(nanoseconds);
}
void AudioMixingSink::create_playback_stream(u32 sample_rate, u32 channel_count)
{
if (m_playback_stream_sample_rate >= sample_rate && m_playback_stream_channel_count >= channel_count) {
@@ -134,14 +111,7 @@ void AudioMixingSink::create_playback_stream(u32 sample_rate, u32 channel_count)
if (new_block.is_empty())
return false;
auto new_block_first_sample_offset = duration_to_sample(new_block.start_timestamp(), sample_rate);
if (!track_data.current_block.is_empty() && track_data.current_block.sample_rate() == sample_rate && track_data.current_block.channel_count() == channel_count) {
auto current_block_end = track_data.current_block_first_sample_offset + static_cast<i64>(track_data.current_block.sample_count());
new_block_first_sample_offset = max(new_block_first_sample_offset, current_block_end);
}
track_data.current_block = move(new_block);
track_data.current_block_first_sample_offset = new_block_first_sample_offset;
return true;
};
@@ -160,7 +130,7 @@ void AudioMixingSink::create_playback_stream(u32 sample_rate, u32 channel_count)
continue;
}
auto first_sample_offset = track_data.current_block_first_sample_offset;
auto first_sample_offset = current_block.timestamp_in_samples();
if (first_sample_offset >= samples_end)
break;
@@ -223,7 +193,7 @@ AK::Duration AudioMixingSink::current_time() const
return m_last_media_time;
auto time = m_last_media_time + (m_playback_stream->total_time_played() - m_last_stream_time);
auto max_time = sample_to_duration(m_next_sample_to_write.load(MemoryOrder::memory_order_acquire), m_playback_stream_sample_rate);
auto max_time = AK::Duration::from_time_units(m_next_sample_to_write.load(MemoryOrder::memory_order_acquire), 1, m_playback_stream_sample_rate);
time = min(time, max_time);
return time;
}
@@ -266,7 +236,7 @@ void AudioMixingSink::pause()
return;
auto new_stream_time = self->m_playback_stream->total_time_played();
auto new_media_time = sample_to_duration(self->m_next_sample_to_write, self->m_playback_stream_sample_rate);
auto new_media_time = AK::Duration::from_time_units(self->m_next_sample_to_write, 1, self->m_playback_stream_sample_rate);
self->m_main_thread_event_loop.deferred_invoke([self, new_stream_time, new_media_time]() {
self->m_last_stream_time = new_stream_time;
@@ -299,13 +269,11 @@ void AudioMixingSink::set_time(AK::Duration time)
{
Threading::MutexLocker mixing_locker { self->m_mutex };
self->m_next_sample_to_write = duration_to_sample(time, self->m_playback_stream_sample_rate);
self->m_next_sample_to_write = time.to_time_units(1, self->m_playback_stream_sample_rate);
}
for (auto& [track, track_data] : self->m_track_mixing_datas) {
for (auto& [track, track_data] : self->m_track_mixing_datas)
track_data.current_block.clear();
track_data.current_block_first_sample_offset = 0;
}
}
if (self->m_playing)

View File

@@ -72,7 +72,6 @@ private:
NonnullRefPtr<AudioDataProvider> provider;
AudioBlock current_block;
i64 current_block_first_sample_offset { NumericLimits<i64>::min() };
};
void deferred_create_playback_stream(Track const& track);

View File

@@ -535,12 +535,12 @@ void initialize_main_thread_vm(AgentType type)
return;
}
// Spec-Note: This step is essentially validating all of the requested module specifiers and type attributes
// when the first call to HostLoadImportedModule for a static module dependency list is made, to
// avoid further loading operations in the case any one of the dependencies has a static error.
// We treat a module with unresolvable module specifiers or unsupported type attributes the same
// as one that cannot be parsed; in both cases, a syntactic issue makes it impossible to ever
// contemplate linking the module later.
// NOTE: This step is essentially validating all of the requested module specifiers and type attributes
// when the first call to HostLoadImportedModule for a static module dependency list is made, to
// avoid further loading operations in the case any one of the dependencies has a static error. We
// treat a module with unresolvable module specifiers or unsupported type attributes the same as
// one that cannot be parsed; in both cases, a syntactic issue makes it impossible to ever
// contemplate linking the module later.
}
}

View File

@@ -119,10 +119,8 @@ WebIDL::ExceptionOr<Utf16String> CSSScale::to_string() const
builder.append(TRY(m_x->to_string()));
// 3. If thiss x and y internal slots are equal numeric values, append ")" to s and return s.
if (m_x->is_equal_numeric_value(m_y)) {
builder.append(")"sv);
return builder.to_utf16_string();
}
// AD-HOC: Don't do this - neither Chrome nor Safari show this behavior.
// Upstream issue: https://github.com/w3c/css-houdini-drafts/issues/1161
// 4. Otherwise, append ", " to s.
builder.append(", "sv);

View File

@@ -4,6 +4,7 @@
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@@ -613,7 +614,7 @@ ValueComparingRefPtr<StyleValue const> interpolate_property(DOM::Element& elemen
return interpolate_repeatable_list(element, calculation_context, from, to, delta, allow_discrete);
case AnimationType::Custom: {
if (property_id == PropertyID::Transform) {
if (auto interpolated_transform = interpolate_transform(element, from, to, delta, allow_discrete))
if (auto interpolated_transform = interpolate_transform(element, calculation_context, from, to, delta, allow_discrete))
return *interpolated_transform;
// https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
@@ -756,83 +757,8 @@ bool property_values_are_transitionable(PropertyID property_id, StyleValue const
return true;
}
// A null return value means the interpolated matrix was not invertible or otherwise invalid
RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete)
static Optional<FloatMatrix4x4> interpolate_matrices(FloatMatrix4x4 const& from, FloatMatrix4x4 const& to, float delta)
{
// Note that the spec uses column-major notation, so all the matrix indexing is reversed.
static constexpr auto make_transformation = [](TransformationStyleValue const& transformation) -> Optional<Transformation> {
Vector<TransformValue> values;
for (auto const& value : transformation.values()) {
switch (value->type()) {
case StyleValue::Type::Angle:
values.append(AngleOrCalculated { value->as_angle().angle() });
break;
case StyleValue::Type::Calculated: {
auto& calculated = value->as_calculated();
if (calculated.resolves_to_angle()) {
values.append(AngleOrCalculated { calculated });
} else if (calculated.resolves_to_length()) {
values.append(LengthPercentage { calculated });
} else if (calculated.resolves_to_number() || calculated.resolves_to_percentage()) {
values.append(NumberPercentage { calculated });
} else {
dbgln("Calculation `{}` inside {} transform-function is not a recognized type", calculated.to_string(SerializationMode::Normal), to_string(transformation.transform_function()));
return {};
}
break;
}
case StyleValue::Type::Length:
values.append(LengthPercentage { value->as_length().length() });
break;
case StyleValue::Type::Percentage:
values.append(LengthPercentage { value->as_percentage().percentage() });
break;
case StyleValue::Type::Number:
values.append(NumberPercentage { Number(Number::Type::Number, value->as_number().number()) });
break;
default:
return {};
}
}
return Transformation { transformation.transform_function(), move(values) };
};
static constexpr auto transformation_style_value_to_matrix = [](DOM::Element& element, TransformationStyleValue const& value) -> Optional<FloatMatrix4x4> {
auto transformation = make_transformation(value);
if (!transformation.has_value())
return {};
Optional<Painting::PaintableBox const&> paintable_box;
if (auto layout_node = element.layout_node()) {
if (auto* paintable = as_if<Painting::PaintableBox>(layout_node->first_paintable()))
paintable_box = *paintable;
}
if (auto matrix = transformation->to_matrix(paintable_box); !matrix.is_error())
return matrix.value();
return {};
};
static constexpr auto style_value_to_matrix = [](DOM::Element& element, StyleValue const& value) -> FloatMatrix4x4 {
if (value.is_transformation())
return transformation_style_value_to_matrix(element, value.as_transformation()).value_or(FloatMatrix4x4::identity());
// This encompasses both the allowed value "none" and any invalid values
if (!value.is_value_list())
return FloatMatrix4x4::identity();
auto matrix = FloatMatrix4x4::identity();
for (auto const& value_element : value.as_value_list().values()) {
if (value_element->is_transformation()) {
if (auto value_matrix = transformation_style_value_to_matrix(element, value_element->as_transformation()); value_matrix.has_value())
matrix = matrix * value_matrix.value();
}
}
return matrix;
};
struct DecomposedValues {
FloatVector3 translation;
FloatVector3 scale;
@@ -1037,20 +963,326 @@ RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, StyleValue
};
};
auto from_matrix = style_value_to_matrix(element, from);
auto to_matrix = style_value_to_matrix(element, to);
auto from_decomposed = decompose(from_matrix);
auto to_decomposed = decompose(to_matrix);
auto from_decomposed = decompose(from);
auto to_decomposed = decompose(to);
if (!from_decomposed.has_value() || !to_decomposed.has_value())
return {};
auto interpolated_decomposed = interpolate(from_decomposed.value(), to_decomposed.value(), delta);
auto interpolated = recompose(interpolated_decomposed);
return recompose(interpolated_decomposed);
}
StyleValueVector values;
values.ensure_capacity(16);
for (int i = 0; i < 16; i++)
values.append(NumberStyleValue::create(static_cast<double>(interpolated[i % 4, i / 4])));
return StyleValueList::create({ TransformationStyleValue::create(PropertyID::Transform, TransformFunction::Matrix3d, move(values)) }, StyleValueList::Separator::Comma);
// https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
RefPtr<StyleValue const> interpolate_transform(DOM::Element& element, CalculationContext const& calculation_context,
StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete)
{
// * If both Va and Vb are none:
// * Vresult is none.
if (from.is_keyword() && from.as_keyword().keyword() == Keyword::None
&& to.is_keyword() && to.as_keyword().keyword() == Keyword::None) {
return KeywordStyleValue::create(Keyword::None);
}
// * Treating none as a list of zero length, if Va or Vb differ in length:
auto style_value_to_transformations = [](StyleValue const& style_value)
-> Vector<NonnullRefPtr<TransformationStyleValue const>> {
if (style_value.is_transformation())
return { style_value.as_transformation() };
// NB: This encompasses both the allowed value "none" and any invalid values.
if (!style_value.is_value_list())
return {};
Vector<NonnullRefPtr<TransformationStyleValue const>> result;
result.ensure_capacity(style_value.as_value_list().size());
for (auto const& value : style_value.as_value_list().values()) {
VERIFY(value->is_transformation());
result.unchecked_append(value->as_transformation());
}
return result;
};
auto from_transformations = style_value_to_transformations(from);
auto to_transformations = style_value_to_transformations(to);
if (from_transformations.size() != to_transformations.size()) {
// * extend the shorter list to the length of the longer list, setting the function at each additional
// position to the identity transform function matching the function at the corresponding position in the
// longer list. Both transform function lists are then interpolated following the next rule.
auto& shorter_list = from_transformations.size() < to_transformations.size() ? from_transformations : to_transformations;
auto const& longer_list = from_transformations.size() < to_transformations.size() ? to_transformations : from_transformations;
for (size_t i = shorter_list.size(); i < longer_list.size(); ++i) {
auto const& transformation = longer_list[i];
shorter_list.append(TransformationStyleValue::identity_transformation(transformation->transform_function()));
}
}
// https://drafts.csswg.org/css-transforms-1/#transform-primitives
auto is_2d_primitive = [](TransformFunction function) {
return first_is_one_of(function,
TransformFunction::Rotate,
TransformFunction::Scale,
TransformFunction::Translate);
};
auto is_2d_transform = [&is_2d_primitive](TransformFunction function) {
return is_2d_primitive(function)
|| first_is_one_of(function,
TransformFunction::ScaleX,
TransformFunction::ScaleY,
TransformFunction::TranslateX,
TransformFunction::TranslateY);
};
// https://drafts.csswg.org/css-transforms-2/#transform-primitives
auto is_3d_primitive = [](TransformFunction function) {
return first_is_one_of(function,
TransformFunction::Rotate3d,
TransformFunction::Scale3d,
TransformFunction::Translate3d);
};
auto is_3d_transform = [&is_2d_transform, &is_3d_primitive](TransformFunction function) {
return is_2d_transform(function)
|| is_3d_primitive(function)
|| first_is_one_of(function,
TransformFunction::RotateX,
TransformFunction::RotateY,
TransformFunction::RotateZ,
TransformFunction::ScaleZ,
TransformFunction::TranslateZ);
};
auto convert_2d_transform_to_primitive = [](NonnullRefPtr<TransformationStyleValue const> transform)
-> NonnullRefPtr<TransformationStyleValue const> {
TransformFunction generic_function;
StyleValueVector parameters;
switch (transform->transform_function()) {
case TransformFunction::Scale:
generic_function = TransformFunction::Scale;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1 ? transform->values()[1] : transform->values()[0]);
break;
case TransformFunction::ScaleX:
generic_function = TransformFunction::Scale;
parameters.append(transform->values()[0]);
parameters.append(NumberStyleValue::create(1.));
break;
case TransformFunction::ScaleY:
generic_function = TransformFunction::Scale;
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Rotate:
generic_function = TransformFunction::Rotate;
parameters.append(transform->values()[0]);
break;
case TransformFunction::Translate:
generic_function = TransformFunction::Translate;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1
? transform->values()[1]
: LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateX:
generic_function = TransformFunction::Translate;
parameters.append(transform->values()[0]);
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateY:
generic_function = TransformFunction::Translate;
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(transform->values()[0]);
break;
default:
VERIFY_NOT_REACHED();
}
return TransformationStyleValue::create(PropertyID::Transform, generic_function, move(parameters));
};
auto convert_3d_transform_to_primitive = [&](NonnullRefPtr<TransformationStyleValue const> transform)
-> NonnullRefPtr<TransformationStyleValue const> {
// NB: Convert to 2D primitive if possible so we don't have to deal with scale/translate X/Y separately.
if (is_2d_transform(transform->transform_function()))
transform = convert_2d_transform_to_primitive(transform);
TransformFunction generic_function;
StyleValueVector parameters;
switch (transform->transform_function()) {
case TransformFunction::Rotate:
case TransformFunction::RotateZ:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::RotateX:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::RotateY:
generic_function = TransformFunction::Rotate3d;
parameters.append(NumberStyleValue::create(0.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(0.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Scale:
generic_function = TransformFunction::Scale3d;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1 ? transform->values()[1] : transform->values()[0]);
parameters.append(NumberStyleValue::create(1.));
break;
case TransformFunction::ScaleZ:
generic_function = TransformFunction::Scale3d;
parameters.append(NumberStyleValue::create(1.));
parameters.append(NumberStyleValue::create(1.));
parameters.append(transform->values()[0]);
break;
case TransformFunction::Translate:
generic_function = TransformFunction::Translate3d;
parameters.append(transform->values()[0]);
parameters.append(transform->values().size() > 1
? transform->values()[1]
: LengthStyleValue::create(Length::make_px(0.)));
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
break;
case TransformFunction::TranslateZ:
generic_function = TransformFunction::Translate3d;
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(LengthStyleValue::create(Length::make_px(0.)));
parameters.append(transform->values()[0]);
break;
default:
VERIFY_NOT_REACHED();
}
return TransformationStyleValue::create(PropertyID::Transform, generic_function, move(parameters));
};
// * Let Vresult be an empty list. Beginning at the start of Va and Vb, compare the corresponding functions at each
// position:
StyleValueVector result;
result.ensure_capacity(from_transformations.size());
size_t index = 0;
for (; index < from_transformations.size(); ++index) {
auto from_transformation = from_transformations[index];
auto to_transformation = to_transformations[index];
auto from_function = from_transformation->transform_function();
auto to_function = to_transformation->transform_function();
// * While the functions have either the same name, or are derivatives of the same primitive transform
// function, interpolate the corresponding pair of functions as described in § 10 Interpolation of
// primitives and derived transform functions and append the result to Vresult.
// https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
// Two different types of transform functions that share the same primitive, or transform functions of the same
// type with different number of arguments can be interpolated. Both transform functions need a former
// conversion to the common primitive first and get interpolated numerically afterwards. The computed value will
// be the primitive with the resulting interpolated arguments.
// The transform functions <matrix()>, matrix3d() and perspective() get converted into 4x4 matrices first and
// interpolated as defined in section Interpolation of Matrices afterwards.
if (first_is_one_of(TransformFunction::Matrix, from_function, to_function)
|| first_is_one_of(TransformFunction::Matrix3d, from_function, to_function)
|| first_is_one_of(TransformFunction::Perspective, from_function, to_function)) {
break;
}
// If both transform functions share a primitive in the two-dimensional space, both transform functions get
// converted to the two-dimensional primitive. If one or both transform functions are three-dimensional
// transform functions, the common three-dimensional primitive is used.
if (is_2d_transform(from_function) && is_2d_transform(to_function)) {
from_transformation = convert_2d_transform_to_primitive(from_transformation);
to_transformation = convert_2d_transform_to_primitive(to_transformation);
} else if (is_3d_transform(from_function) || is_3d_transform(to_function)) {
// NB: 3D primitives do not support value expansion like their 2D counterparts do (e.g. scale(1.5) ->
// scale(1.5, 1.5), so we check if they are already a primitive first.
if (!is_3d_primitive(from_function))
from_transformation = convert_3d_transform_to_primitive(from_transformation);
if (!is_3d_primitive(to_function))
to_transformation = convert_3d_transform_to_primitive(to_transformation);
}
from_function = from_transformation->transform_function();
to_function = to_transformation->transform_function();
// NB: We converted both functions to their primitives. But if they're different primitives or if they have a
// different number of values, we can't interpolate numerically between them. Break here so the next loop
// can take care of the remaining functions.
auto const& from_values = from_transformation->values();
auto const& to_values = to_transformation->values();
if (from_function != to_function || from_values.size() != to_values.size())
break;
// https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
if (from_function == TransformFunction::Rotate3d) {
// FIXME: For interpolations with the primitive rotate3d(), the direction vectors of the transform functions get
// normalized first. If the normalized vectors are not equal and both rotation angles are non-zero the
// transform functions get converted into 4x4 matrices first and interpolated as defined in section
// Interpolation of Matrices afterwards. Otherwise the rotation angle gets interpolated numerically and the
// rotation vector of the non-zero angle is used or (0, 0, 1) if both angles are zero.
auto interpolated_rotation = interpolate_rotate(element, calculation_context, from_transformation,
to_transformation, delta, AllowDiscrete::No);
if (!interpolated_rotation)
break;
result.unchecked_append(*interpolated_rotation);
} else {
StyleValueVector interpolated;
interpolated.ensure_capacity(from_values.size());
for (size_t i = 0; i < from_values.size(); ++i) {
auto interpolated_value = interpolate_value(element, calculation_context, from_values[i], to_values[i],
delta, AllowDiscrete::No);
if (!interpolated_value)
break;
interpolated.unchecked_append(*interpolated_value);
}
if (interpolated.size() != from_values.size())
break;
result.unchecked_append(TransformationStyleValue::create(PropertyID::Transform, from_function, move(interpolated)));
}
}
// NB: Return if we're done.
if (index == from_transformations.size())
return StyleValueList::create(move(result), StyleValueList::Separator::Space);
// * If the pair do not have a common name or primitive transform function, post-multiply the remaining
// transform functions in each of Va and Vb respectively to produce two 4x4 matrices. Interpolate these two
// matrices as described in § 11 Interpolation of Matrices, append the result to Vresult, and cease
// iterating over Va and Vb.
Optional<Painting::PaintableBox const&> paintable_box;
if (auto* paintable = as_if<Painting::PaintableBox>(element.paintable()))
paintable_box = *paintable;
auto post_multiply_remaining_transformations = [&paintable_box](size_t start_index, Vector<NonnullRefPtr<TransformationStyleValue const>> const& transformations) {
FloatMatrix4x4 result = FloatMatrix4x4::identity();
for (auto index = start_index; index < transformations.size(); ++index) {
auto transformation = transformations[index]->to_transformation();
auto transformation_matrix = transformation.to_matrix(paintable_box);
if (transformation_matrix.is_error()) {
dbgln("Unable to interpret a transformation's matrix; bailing out of interpolation.");
break;
}
result = result * transformation_matrix.value();
}
return result;
};
auto from_matrix = post_multiply_remaining_transformations(index, from_transformations);
auto to_matrix = post_multiply_remaining_transformations(index, to_transformations);
auto maybe_interpolated_matrix = interpolate_matrices(from_matrix, to_matrix, delta);
if (maybe_interpolated_matrix.has_value()) {
auto interpolated_matrix = maybe_interpolated_matrix.release_value();
StyleValueVector values;
values.ensure_capacity(16);
for (int i = 0; i < 16; i++)
values.unchecked_append(NumberStyleValue::create(interpolated_matrix[i % 4, i / 4]));
result.append(TransformationStyleValue::create(PropertyID::Transform, TransformFunction::Matrix3d, move(values)));
} else {
dbgln("Unable to interpolate matrices.");
}
return StyleValueList::create(move(result), StyleValueList::Separator::Space);
}
Color interpolate_color(Color from, Color to, float delta, ColorSyntax syntax)

View File

@@ -29,7 +29,7 @@ Optional<LengthPercentageOrAuto> interpolate_length_percentage_or_auto(Calculati
RefPtr<StyleValue const> interpolate_value(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_repeatable_list(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_box_shadow(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_transform(DOM::Element&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
RefPtr<StyleValue const> interpolate_transform(DOM::Element&, CalculationContext const&, StyleValue const& from, StyleValue const& to, float delta, AllowDiscrete);
Color interpolate_color(Color from, Color to, float delta, ColorSyntax syntax);

View File

@@ -2880,7 +2880,7 @@ RefPtr<StyleValue const> Parser::parse_font_language_override_value(TokenStream<
{
// https://drafts.csswg.org/css-fonts/#propdef-font-language-override
// This is `normal | <string>` but with the constraint that the string has to be 4 characters long:
// Shorter strings are right-padded with spaces, and longer strings are invalid.
// Shorter strings are right-padded with spaces before use, and longer strings are invalid.
if (auto normal = parse_all_as_single_keyword_value(tokens, Keyword::Normal))
return normal;
@@ -2927,9 +2927,20 @@ RefPtr<StyleValue const> Parser::parse_font_language_override_value(TokenStream<
});
return nullptr;
}
// We're expected to always serialize without any trailing spaces, so remove them now for convenience.
auto trimmed = string_value.bytes_as_string_view().trim_whitespace(TrimMode::Right);
if (trimmed.is_empty()) {
ErrorReporter::the().report(InvalidPropertyError {
.rule_name = "style"_fly_string,
.property_name = "font-language-override"_fly_string,
.value_string = tokens.dump_string(),
.description = MUST(String::formatted("<string> value \"{}\" is only whitespace", string_value)),
});
return nullptr;
}
transaction.commit();
if (length < 4)
return StringStyleValue::create(MUST(String::formatted("{:<4}", string_value)));
if (trimmed != string_value.bytes_as_string_view())
return StringStyleValue::create(FlyString::from_utf8_without_validation(trimmed.bytes()));
return string;
}

View File

@@ -191,9 +191,7 @@ public:
String dump_string()
{
// FIXME: The whitespace is only needed because we strip it when parsing property values. Remove it here once
// we stop doing that.
return MUST(String::join(" "sv, m_tokens));
return MUST(String::join(""sv, m_tokens));
}
private:

View File

@@ -34,7 +34,7 @@ String SimpleBlock::to_string() const
StringBuilder builder;
builder.append(token.bracket_string());
builder.join(' ', value);
builder.join(""sv, value);
builder.append(token.bracket_mirror_string());
return builder.to_string_without_validation();

View File

@@ -4,11 +4,11 @@
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2022-2023, MacDue <macdue@dueutil.tech>
* Copyright (c) 2024, Steffen T. Larssen <dudedbz@gmail.com>
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "TransformationStyleValue.h"
#include <AK/StringBuilder.h>
#include <LibWeb/CSS/CSSMatrixComponent.h>
#include <LibWeb/CSS/CSSPerspective.h>
@@ -23,49 +23,121 @@
#include <LibWeb/CSS/PropertyID.h>
#include <LibWeb/CSS/Serialize.h>
#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
#include <LibWeb/CSS/Transformation.h>
#include <LibWeb/Geometry/DOMMatrix.h>
namespace Web::CSS {
ValueComparingNonnullRefPtr<TransformationStyleValue const> TransformationStyleValue::identity_transformation(
TransformFunction transform_function)
{
// https://drafts.csswg.org/css-transforms-1/#identity-transform-function
// A transform function that is equivalent to a identity 4x4 matrix (see Mathematical Description of Transform
// Functions). Examples for identity transform functions are translate(0), translateX(0), translateY(0), scale(1),
// scaleX(1), scaleY(1), rotate(0), skew(0, 0), skewX(0), skewY(0) and matrix(1, 0, 0, 1, 0, 0).
// https://drafts.csswg.org/css-transforms-2/#identity-transform-function
// In addition to the identity transform function in CSS Transforms, examples for identity transform functions
// include translate3d(0, 0, 0), translateZ(0), scaleZ(1), rotate3d(1, 1, 1, 0), rotateX(0), rotateY(0), rotateZ(0)
// and matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1). A special case is perspective: perspective(none).
// The value of m34 becomes infinitesimal small and the transform function is therefore assumed to be equal to the
// identity matrix.
auto identity_parameters = [&] -> StyleValueVector {
auto const number_zero = NumberStyleValue::create(0.);
auto const number_one = NumberStyleValue::create(1.);
switch (transform_function) {
case TransformFunction::Matrix:
return { number_one, number_zero, number_zero, number_one, number_zero, number_zero };
case TransformFunction::Matrix3d:
return { number_one, number_zero, number_zero, number_zero,
number_zero, number_one, number_zero, number_zero,
number_zero, number_zero, number_one, number_zero,
number_zero, number_zero, number_zero, number_one };
case TransformFunction::Perspective:
return { KeywordStyleValue::create(Keyword::None) };
case TransformFunction::Rotate:
case TransformFunction::RotateX:
case TransformFunction::RotateY:
case TransformFunction::RotateZ:
return { AngleStyleValue::create(Angle::make_degrees(0.)) };
case TransformFunction::Rotate3d:
return { number_one, number_one, number_one, AngleStyleValue::create(Angle::make_degrees(0.)) };
case TransformFunction::Skew:
case TransformFunction::SkewX:
case TransformFunction::SkewY:
case TransformFunction::Translate:
case TransformFunction::TranslateX:
case TransformFunction::TranslateY:
case TransformFunction::TranslateZ:
return { LengthStyleValue::create(Length::make_px(0.)) };
case TransformFunction::Translate3d:
return {
LengthStyleValue::create(Length::make_px(0.)),
LengthStyleValue::create(Length::make_px(0.)),
LengthStyleValue::create(Length::make_px(0.)),
};
case TransformFunction::Scale:
case TransformFunction::ScaleX:
case TransformFunction::ScaleY:
case TransformFunction::ScaleZ:
return { number_one };
case TransformFunction::Scale3d:
return { number_one, number_one, number_one };
}
VERIFY_NOT_REACHED();
};
return create(PropertyID::Transform, transform_function, identity_parameters());
}
Transformation TransformationStyleValue::to_transformation() const
{
auto function_metadata = transform_function_metadata(m_properties.transform_function);
Vector<TransformValue> values;
values.ensure_capacity(m_properties.values.size());
size_t argument_index = 0;
for (auto& transformation_value : m_properties.values) {
auto const function_type = function_metadata.parameters[argument_index].type;
auto const function_type = function_metadata.parameters[argument_index++].type;
if (transformation_value->is_calculated()) {
auto& calculated = transformation_value->as_calculated();
if (calculated.resolves_to_length()) {
values.append(LengthPercentage { calculated });
values.unchecked_append(LengthPercentage { calculated });
} else if (calculated.resolves_to_number() || calculated.resolves_to_percentage()) {
values.append(NumberPercentage { calculated });
values.unchecked_append(NumberPercentage { calculated });
} else if (calculated.resolves_to_angle()) {
values.append(AngleOrCalculated { calculated });
values.unchecked_append(AngleOrCalculated { calculated });
} else {
dbgln("FIXME: Unsupported calc value in transform! {}", calculated.to_string(SerializationMode::Normal));
}
} else if (transformation_value->is_keyword()) {
if (function_type == TransformFunctionParameterType::LengthNone
&& transformation_value->as_keyword().keyword() == Keyword::None) {
// We don't add 'none' to the list of values.
} else {
dbgln("FIXME: Unsupported keyword value '{}' in transform", transformation_value->to_string(SerializationMode::Normal));
}
} else if (transformation_value->is_length()) {
values.append({ transformation_value->as_length().length() });
values.unchecked_append({ transformation_value->as_length().length() });
} else if (transformation_value->is_percentage()) {
if (function_type == TransformFunctionParameterType::NumberPercentage) {
values.append(NumberPercentage { transformation_value->as_percentage().percentage() });
values.unchecked_append(NumberPercentage { transformation_value->as_percentage().percentage() });
} else {
values.append(LengthPercentage { transformation_value->as_percentage().percentage() });
values.unchecked_append(LengthPercentage { transformation_value->as_percentage().percentage() });
}
} else if (transformation_value->is_number()) {
values.append({ Number(Number::Type::Number, transformation_value->as_number().number()) });
values.unchecked_append({ Number(Number::Type::Number, transformation_value->as_number().number()) });
} else if (transformation_value->is_angle()) {
values.append({ transformation_value->as_angle().angle() });
values.unchecked_append({ transformation_value->as_angle().angle() });
} else {
dbgln("FIXME: Unsupported value in transform! {}", transformation_value->to_string(SerializationMode::Normal));
}
argument_index++;
}
return Transformation { m_properties.transform_function, move(values) };
}

View File

@@ -22,6 +22,8 @@ public:
}
virtual ~TransformationStyleValue() override = default;
static ValueComparingNonnullRefPtr<TransformationStyleValue const> identity_transformation(TransformFunction);
TransformFunction transform_function() const { return m_properties.transform_function; }
StyleValueVector const& values() const { return m_properties.values; }

View File

@@ -90,10 +90,7 @@ ErrorOr<Gfx::FloatMatrix4x4> Transformation::to_matrix(Optional<Painting::Painta
0, 0, 1, 0,
0, 0, -1 / distance, 1);
}
return Gfx::FloatMatrix4x4(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::Matrix:
if (count == 6)
return Gfx::FloatMatrix4x4(TRY(value(0)), TRY(value(2)), 0, TRY(value(4)),

View File

@@ -146,7 +146,7 @@ struct RsaKeyGenParams : public AlgorithmParams {
}
u32 modulus_length;
// NOTE that the raw data is going to be in Big Endian u8[] format
// NOTE: The raw data is going to be in Big Endian u8[] format
::Crypto::UnsignedBigInteger public_exponent;
static JS::ThrowCompletionOr<NonnullOwnPtr<AlgorithmParams>> from_value(JS::VM&, JS::Value);

View File

@@ -734,16 +734,18 @@ JS::ThrowCompletionOr<void> EventTarget::process_event_handler_for_event(FlyStri
if (special_error_event_handling) {
// -> If special error event handling is true
// If return value is true, then set event's canceled flag.
// NOTE: the return type of EventHandler is `any`, so no coercion happens, meaning we have to check if it's a boolean first.
// NB: the return type of EventHandler is `any`, so no coercion happens, meaning we have to check if it's a
// boolean first.
if (return_value.is_boolean() && return_value.as_bool())
event.set_cancelled(true);
} else {
// -> Otherwise
// If return value is false, then set event's canceled flag.
// NOTE: the return type of EventHandler is `any`, so no coercion happens, meaning we have to check if it's a boolean first.
// Spec-Note: If we've gotten to this "Otherwise" clause because event's type is "beforeunload" but event is
// not a BeforeUnloadEvent object, then return value will never be false, since in such cases
// return value will have been coerced into either null or a DOMString.
// NB: the return type of EventHandler is `any`, so no coercion happens, meaning we have to check if it's a
// boolean first.
// NOTE: If we've gotten to this "Otherwise" clause because event's type is "beforeunload" but event is not a
// BeforeUnloadEvent object, then return value will never be false, since in such cases return value will
// have been coerced into either null or a DOMString.
if (return_value.is_boolean() && !return_value.as_bool() && !is_beforeunload)
event.set_cancelled(true);
}

View File

@@ -786,10 +786,10 @@ void Node::insert_before(GC::Ref<Node> node, GC::Ptr<Node> child, bool suppress_
children_changed(&metadata);
// 10. Let staticNodeList be a list of nodes, initially « ».
// Spec-Note: We collect all nodes before calling the post-connection steps on any one of them, instead of calling
// the post-connection steps while were traversing the node tree. This is because the post-connection
// steps can modify the trees structure, making live traversal unsafe, possibly leading to the
// post-connection steps being called multiple times on the same node.
// NOTE: We collect all nodes before calling the post-connection steps on any one of them, instead of calling the
// post-connection steps while were traversing the node tree. This is because the post-connection steps can
// modify the trees structure, making live traversal unsafe, possibly leading to the post-connection steps
// being called multiple times on the same node.
GC::RootVector<GC::Ref<Node>> static_node_list(heap());
// 11. For each node of nodes, in tree order:
@@ -874,11 +874,6 @@ WebIDL::ExceptionOr<GC::Ref<Node>> Node::append_child(GC::Ref<Node> node)
{
// To append a node to a parent, pre-insert node into parent before null.
return pre_insert(node, nullptr);
// AD-HOC: invalidate the ordinal of the first list_item of the first child sibling of the appended node, if any.
// NOTE: This works since ordinal values are accessed (for layout and paint) in the preorder of list_item nodes !!
if (auto* first_child_element = this->first_child_of_type<Element>())
first_child_element->maybe_invalidate_ordinals_for_list_owner();
}
// https://dom.spec.whatwg.org/#live-range-pre-remove-steps

View File

@@ -258,7 +258,8 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
if (m_state != State::Loading)
dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadend));
// Spec-Note: Event handler for the load or error events could have started another load, if that happens the loadend event for this load is not fired.
// NOTE: Event handler for the load or error events could have started another load, if that happens
// the loadend event for this load is not fired.
}));
return;
@@ -278,7 +279,8 @@ WebIDL::ExceptionOr<void> FileReader::read_operation(Blob& blob, Type type, Opti
if (m_state != State::Loading)
dispatch_event(DOM::Event::create(realm, HTML::EventNames::loadend));
// Spec-Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired.
// NOTE: Event handler for the error event could have started another load, if that happens the
// loadend event for this load is not fired.
}));
return;

View File

@@ -154,9 +154,9 @@ bool HTMLButtonElement::has_activation_behavior() const
return true;
}
// https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element:activation-behaviour
void HTMLButtonElement::activation_behavior(DOM::Event const& event)
{
// https://html.spec.whatwg.org/multipage/form-elements.html#the-button-element:activation-behaviour
// 1. If element is disabled, then return.
if (!enabled())
return;
@@ -223,8 +223,12 @@ void HTMLButtonElement::activation_behavior(DOM::Event const& event)
return;
}
// 5. Let continue be the result of firing an event named command at target, using CommandEvent, with its command attribute initialized to command, its source attribute initialized to element, and its cancelable and composed attributes initialized to true.
// SPEC-NOTE: DOM standard issue #1328 tracks how to better standardize associated event data in a way which makes sense on Events. Currently an event attribute initialized to a value cannot also have a getter, and so an internal slot (or map of additional fields) is required to properly specify this.
// 5. Let continue be the result of firing an event named command at target, using CommandEvent, with its
// command attribute initialized to command, its source attribute initialized to element, and its cancelable
// and composed attributes initialized to true.
// NOTE: DOM standard issue #1328 tracks how to better standardize associated event data in a way which makes
// sense on Events. Currently an event attribute initialized to a value cannot also have a getter, and so
// an internal slot (or map of additional fields) is required to properly specify this.
CommandEventInit event_init {};
event_init.command = command;
event_init.source = this;

View File

@@ -244,10 +244,10 @@ WebIDL::ExceptionOr<NavigationResult> Navigation::navigate(String url, Navigatio
// 4. Let state be options["state"], if it exists; otherwise, undefined.
auto state = options.state.value_or(JS::js_undefined());
// 5. Let serializedState be StructuredSerializeForStorage(state).
// If this throws an exception, then return an early error result for that exception.
// Spec-Note: It is important to perform this step early, since serialization can invoke web developer code,
// which in turn might change various things we check in later steps.
// 5. Let serializedState be StructuredSerializeForStorage(state). If this throws an exception, then return an early
// error result for that exception.
// NOTE: It is important to perform this step early, since serialization can invoke web developer code, which in
// turn might change various things we check in later steps.
auto serialized_state_or_error = structured_serialize_for_storage(vm, state);
if (serialized_state_or_error.is_error()) {
return early_error_result(serialized_state_or_error.release_error());
@@ -307,10 +307,10 @@ WebIDL::ExceptionOr<NavigationResult> Navigation::reload(NavigationReloadOptions
// 2. Let serializedState be StructuredSerializeForStorage(undefined).
auto serialized_state = MUST(structured_serialize_for_storage(vm, JS::js_undefined()));
// 3. If options["state"] exists, then set serializedState to StructuredSerializeForStorage(options["state"]).
// If this throws an exception, then return an early error result for that exception.
// Spec-Note: It is important to perform this step early, since serialization can invoke web developer
// code, which in turn might change various things we check in later steps.
// 3. If options["state"] exists, then set serializedState to StructuredSerializeForStorage(options["state"]). If
// this throws an exception, then return an early error result for that exception.
// NOTE: It is important to perform this step early, since serialization can invoke web developer code, which in
// turn might change various things we check in later steps.
if (options.state.has_value()) {
auto serialized_state_or_error = structured_serialize_for_storage(vm, options.state.value());
if (serialized_state_or_error.is_error())

View File

@@ -157,7 +157,8 @@ WebIDL::ExceptionOr<URL::URL> resolve_module_specifier(Optional<Script&> referri
result = TRY(resolve_imports_match(normalized_specifier.to_byte_string(), as_url, import_map.imports()));
// 12. If result is null, set it to asURL.
// Spec-Note: By this point, if result was null, specifier wasn't remapped to anything by importMap, but it might have been able to be turned into a URL.
// NOTE: By this point, if result was null, specifier wasn't remapped to anything by importMap, but it might have
// been able to be turned into a URL.
if (!result.has_value())
result = as_url;

View File

@@ -300,8 +300,8 @@ void merge_existing_and_new_import_maps(Window& global, ImportMap& new_import_ma
// 1. Let newImportMapScopes be a deep copy of newImportMap's scopes.
auto new_import_map_scopes = new_import_map.scopes();
// Spec-Note: We're mutating these copies and removing items from them when they are used to ignore scope-specific
// rules. This is true for newImportMapScopes, as well as to newImportMapImports below.
// NOTE: We're mutating these copies and removing items from them when they are used to ignore scope-specific rules.
// This is true for newImportMapScopes, as well as to newImportMapImports below.
// 2. Let oldImportMap be global's import map.
auto& old_import_map = global.import_map();

View File

@@ -58,8 +58,8 @@ struct SpecifierResolution {
// A specifier as a URL
// A URL-or-null that represents the URL in case of a URL-like module specifier.
//
// Spec-Note: Implementations can replace specifier as a URL with a boolean that indicates
// that the specifier is either bare or URL-like that is special.
// NOTE: Implementations can replace specifier as a URL with a boolean that indicates that the specifier is either
// bare or URL-like that is special.
bool specifier_is_null_or_url_like_that_is_special { false };
};
@@ -300,10 +300,10 @@ private:
// https://html.spec.whatwg.org/multipage/webappapis.html#resolved-module-set
// A global object has a resolved module set, a set of specifier resolution records, initially empty.
//
// Spec-Note: The resolved module set ensures that module specifier resolution returns the same result when called
// multiple times with the same (referrer, specifier) pair. It does that by ensuring that import map rules
// that impact the specifier in its referrer's scope cannot be defined after its initial resolution. For
// now, only Window global objects have their module set data structures modified from the initial empty one.
// NOTE: The resolved module set ensures that module specifier resolution returns the same result when called
// multiple times with the same (referrer, specifier) pair. It does that by ensuring that import map rules
// that impact the specifier in its referrer's scope cannot be defined after its initial resolution. For now,
// only Window global objects have their module set data structures modified from the initial empty one.
Vector<SpecifierResolution> m_resolved_module_set;
GC::Ptr<CSS::Screen> m_screen;

View File

@@ -258,7 +258,7 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
else HANDLE_COMMAND(PaintNestedDisplayList, paint_nested_display_list)
else HANDLE_COMMAND(ApplyOpacity, apply_opacity)
else HANDLE_COMMAND(ApplyCompositeAndBlendingOperator, apply_composite_and_blending_operator)
else HANDLE_COMMAND(ApplyFilter, apply_filters)
else HANDLE_COMMAND(ApplyFilter, apply_filter)
else HANDLE_COMMAND(ApplyTransform, apply_transform)
else HANDLE_COMMAND(ApplyMaskBitmap, apply_mask_bitmap)
else VERIFY_NOT_REACHED();

View File

@@ -68,7 +68,7 @@ private:
virtual void paint_scrollbar(PaintScrollBar const&) = 0;
virtual void apply_opacity(ApplyOpacity const&) = 0;
virtual void apply_composite_and_blending_operator(ApplyCompositeAndBlendingOperator const&) = 0;
virtual void apply_filters(ApplyFilter const&) = 0;
virtual void apply_filter(ApplyFilter const&) = 0;
virtual void apply_transform(ApplyTransform const&) = 0;
virtual void apply_mask_bitmap(ApplyMaskBitmap const&) = 0;
virtual bool would_be_fully_clipped_by_painter(Gfx::IntRect) const = 0;

View File

@@ -95,7 +95,7 @@ void AddClipRect::dump(StringBuilder& builder) const
void PushStackingContext::dump(StringBuilder& builder) const
{
auto affine_transform = extract_2d_affine_transform(transform.matrix);
builder.appendff("PushStackingContext opacity={} isolate={} has_clip_path={} transform={}", opacity, isolate, clip_path.has_value(), affine_transform);
builder.appendff("PushStackingContext opacity={} isolate={} has_clip_path={} transform={} bounding_rect={}", opacity, isolate, clip_path.has_value(), affine_transform, bounding_rect);
}
void PopStackingContext::dump(StringBuilder& builder) const

View File

@@ -617,7 +617,7 @@ static SkTileMode to_skia_tile_mode(SVGLinearGradientPaintStyle::SpreadMethod sp
}
}
static SkPaint paint_style_to_skia_paint(Painting::SVGGradientPaintStyle const& paint_style, Gfx::FloatRect bounding_rect)
static SkPaint paint_style_to_skia_paint(Painting::SVGGradientPaintStyle const& paint_style, Gfx::FloatRect const& bounding_rect)
{
SkPaint paint;
@@ -968,7 +968,7 @@ void DisplayListPlayerSkia::apply_composite_and_blending_operator(ApplyComposite
canvas.saveLayer(nullptr, &paint);
}
void DisplayListPlayerSkia::apply_filters(ApplyFilter const& command)
void DisplayListPlayerSkia::apply_filter(ApplyFilter const& command)
{
sk_sp<SkImageFilter> image_filter = to_skia_image_filter(command.filter);

View File

@@ -55,7 +55,7 @@ private:
void paint_nested_display_list(PaintNestedDisplayList const&) override;
void apply_opacity(ApplyOpacity const&) override;
void apply_composite_and_blending_operator(ApplyCompositeAndBlendingOperator const&) override;
void apply_filters(ApplyFilter const&) override;
void apply_filter(ApplyFilter const&) override;
void apply_transform(ApplyTransform const&) override;
void apply_mask_bitmap(ApplyMaskBitmap const&) override;

View File

@@ -7,7 +7,6 @@
#pragma once
#include <AK/Vector.h>
#include <LibGfx/Matrix4x4.h>
#include <LibWeb/Export.h>
#include <LibWeb/Painting/Paintable.h>

View File

@@ -41,9 +41,9 @@ void ViewportPaintable::build_stacking_context_tree()
set_stacking_context(make<StackingContext>(*this, nullptr, 0));
size_t index_in_tree_order = 1;
for_each_in_subtree_of_type<PaintableBox>([&](auto const& paintable_box) {
const_cast<PaintableBox&>(paintable_box).invalidate_stacking_context();
auto* parent_context = const_cast<PaintableBox&>(paintable_box).enclosing_stacking_context();
for_each_in_subtree_of_type<PaintableBox>([&](auto& paintable_box) {
paintable_box.invalidate_stacking_context();
auto* parent_context = paintable_box.enclosing_stacking_context();
auto establishes_stacking_context = paintable_box.layout_node().establishes_stacking_context();
if ((paintable_box.is_positioned() || establishes_stacking_context) && paintable_box.computed_values().z_index().value_or(0) == 0)
parent_context->m_positioned_descendants_and_stacking_contexts_with_stack_level_0.append(paintable_box);
@@ -54,7 +54,7 @@ void ViewportPaintable::build_stacking_context_tree()
return TraversalDecision::Continue;
}
VERIFY(parent_context);
const_cast<PaintableBox&>(paintable_box).set_stacking_context(make<Painting::StackingContext>(const_cast<PaintableBox&>(paintable_box), parent_context, index_in_tree_order++));
paintable_box.set_stacking_context(make<StackingContext>(paintable_box, parent_context, index_in_tree_order++));
return TraversalDecision::Continue;
});

View File

@@ -63,8 +63,8 @@ void SVGAnimatedNumber::set_base_val(float new_value)
// 3. Let second be the second number in current if it has been explicitly specified, and if not, the implicit
// value as described in the definition of the attribute.
// LB-NOTE: All known usages of <number-optional-number> specify that a missing second number defaults to the
// value of the first number.
// NB: All known usages of <number-optional-number> specify that a missing second number defaults to the value
// of the first number.
auto second = current_values.size() > 1 && !current_values[1].is_empty()
? parse_value_or_initial(current_values[1])
: first;
@@ -130,8 +130,8 @@ float SVGAnimatedNumber::get_base_or_anim_value() const
// 2. Otherwise, this SVGAnimatedNumber object reflects the second number. Return the second value in value if
// it has been explicitly specified, and if not, return the implicit value as described in the definition of
// the attribute.
// LB-NOTE: All known usages of <number-optional-number> specify that a missing second number defaults to the
// value of the first number.
// NB: All known usages of <number-optional-number> specify that a missing second number defaults to the value
// of the first number.
VERIFY(m_value_represented == ValueRepresented::Second);
if (values.size() > 1 && !values[1].is_empty())
return parse_value_or_initial(values[1]);

View File

@@ -499,9 +499,8 @@ static void run_job(JS::VM& vm, JobQueue& job_queue)
});
// FIXME: How does the user agent ensure this happens? Is this a normative note?
// Spec-Note:
// For a register job and an update job, the user agent delays queuing a task for running the job
// until after a DOMContentLoaded event has been dispatched to the document that initiated the job.
// NOTE: For a register job and an update job, the user agent delays queuing a task for running the job until after
// a DOMContentLoaded event has been dispatched to the document that initiated the job.
// FIXME: Spec should be updated to avoid 'queue a task' and use 'queue a global task' instead
// FIXME: On which task source? On which event loop? On behalf of which document?

View File

@@ -58,7 +58,7 @@ LOCAL_INCLUDE_SUFFIX_EXCLUDES = [
SINGLE_PAGE_HTML_SPEC_LINK = re.compile("//.*https://html\\.spec\\.whatwg\\.org/#")
# We similarily check and disallow AD-HOCs and FIXMEs that aren't followed by a colon.
INVALID_AD_HOC_OR_FIXME = re.compile(r'^(?:[\s\d./\-(*]+(?:AD-HOC|FIXME)[^:]|.*"FIXME[^:"]).*$', re.MULTILINE)
INVALID_AD_HOC_OR_FIXME = re.compile(r'^(?:[\s\d./\-(*]+(?:AD-HOC|FIXME|NB|NOTE)[^:]|.*"FIXME[^:"]).*$', re.MULTILINE)
def should_check_file(filename):

View File

@@ -49,3 +49,43 @@ TEST_CASE(enumerate)
EXPECT_EQ(result, (Vector<IndexAndValue> { { 0, 9 }, { 1, 8 }, { 2, 7 }, { 3, 6 } }));
}
}
class CopyCounter {
public:
static inline size_t copy_count = 0;
CopyCounter() = default;
CopyCounter(CopyCounter const&) { ++copy_count; }
CopyCounter(CopyCounter&&) { }
auto begin() const { return m_vec.begin(); }
auto end() const { return m_vec.end(); }
private:
Vector<int> m_vec { 1, 2, 3, 4 };
};
TEST_CASE(do_not_copy)
{
{
Vector<IndexAndValue> result;
CopyCounter::copy_count = 0;
CopyCounter counter {};
for (auto [i, value] : enumerate(counter))
result.append({ i, value });
EXPECT_EQ(result, (Vector<IndexAndValue> { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 4 } }));
EXPECT_EQ(CopyCounter::copy_count, 0uz);
}
{
Vector<IndexAndValue> result;
CopyCounter::copy_count = 0;
for (auto [i, value] : enumerate(CopyCounter {}))
result.append({ i, value });
EXPECT_EQ(result, (Vector<IndexAndValue> { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 4 } }));
EXPECT_EQ(CopyCounter::copy_count, 0uz);
}
}

View File

@@ -939,3 +939,44 @@ TEST_CASE(from_f64_seconds)
EXPECT_DEATH("Converting float NaN seconds", (void)Duration::from_seconds_f64(NAN));
}
TEST_CASE(time_units)
{
EXPECT_EQ(Duration::from_time_units(1, 1, 1), Duration::from_seconds(1));
EXPECT_EQ(Duration::from_time_units(-312, 1, 48'000), Duration::from_microseconds(-6'500));
EXPECT_EQ(Duration::from_time_units(960, 1, 48'000), Duration::from_microseconds(20'000));
EXPECT_EQ(Duration::from_time_units(960, 1, 48'000), Duration::from_microseconds(20'000));
EXPECT_EQ(Duration::from_time_units(8, 4, 1), Duration::from_seconds(32));
EXPECT_EQ(Duration::from_time_units(3, 3, 2'000'000'000), Duration::from_nanoseconds(5));
EXPECT_EQ(Duration::from_time_units(4, 3, 2'000'000'000), Duration::from_nanoseconds(6));
EXPECT_EQ(Duration::from_time_units(999'999'998, 1, 2'000'000'000), Duration::from_nanoseconds(499'999'999));
EXPECT_EQ(Duration::from_time_units(999'999'999, 1, 2'000'000'000), Duration::from_nanoseconds(500'000'000));
EXPECT_EQ(Duration::from_time_units(1'000'000'000, 1, 2'000'000'000), Duration::from_nanoseconds(500'000'000));
EXPECT_EQ(Duration::from_time_units(NumericLimits<i64>::max(), 1, 2), Duration::from_seconds(NumericLimits<i64>::max() / 2) + Duration::from_milliseconds(500));
EXPECT_EQ(Duration::from_time_units((NumericLimits<i64>::max() / 2), 2, 1), Duration::from_seconds(NumericLimits<i64>::max() - 1));
EXPECT_EQ(Duration::from_time_units((NumericLimits<i64>::max() / 2) + 1, 2, 1), Duration::from_seconds(NumericLimits<i64>::max()));
EXPECT_EQ(Duration::from_time_units((NumericLimits<i64>::min() / 2), 2, 1), Duration::from_seconds(NumericLimits<i64>::min()));
EXPECT_EQ(Duration::from_time_units((NumericLimits<i64>::min() / 2) - 1, 2, 1), Duration::from_seconds(NumericLimits<i64>::min()));
EXPECT_EQ(Duration::from_milliseconds(999).to_time_units(1, 48'000), 47'952);
EXPECT_EQ(Duration::from_milliseconds(-12'500).to_time_units(1, 1'000), -12'500);
EXPECT_EQ(Duration::from_milliseconds(-12'500).to_time_units(1, 1'000), -12'500);
EXPECT_EQ(Duration::from_nanoseconds(154'489'696).to_time_units(1, 48'000), 7'416);
EXPECT_EQ(Duration::from_nanoseconds(154'489'375).to_time_units(1, 48'000), 7'415);
EXPECT_EQ(Duration::from_nanoseconds(-154'489'696).to_time_units(1, 48'000), -7'416);
EXPECT_EQ(Duration::from_nanoseconds(-154'489'375).to_time_units(1, 48'000), -7'415);
EXPECT_EQ(Duration::from_nanoseconds(1'900'000'000).to_time_units(3, 2), 1);
EXPECT_EQ(Duration::from_nanoseconds(1'800'000'000).to_time_units(3, 1), 1);
EXPECT_EQ(Duration::from_seconds(3).to_time_units(4, 1), 1);
EXPECT_EQ(Duration::from_seconds(4).to_time_units(4, 1), 1);
EXPECT_EQ(Duration::from_seconds(5).to_time_units(4, 1), 1);
EXPECT_EQ(Duration::from_seconds(6).to_time_units(4, 1), 2);
EXPECT_EQ(Duration::from_seconds(2'147'483'649).to_time_units(1, NumericLimits<u32>::max()), NumericLimits<i64>::max());
EXPECT_EQ(Duration::from_seconds(2'147'483'648).to_time_units(1, NumericLimits<u32>::max()), NumericLimits<i64>::max() - (NumericLimits<u32>::max() / 2));
EXPECT_DEATH("From time units with zero numerator", (void)Duration::from_time_units(1, 0, 1));
EXPECT_DEATH("From time units with zero denominator", (void)Duration::from_time_units(1, 1, 0));
}

View File

@@ -90,6 +90,7 @@ static inline void decode_audio(StringView path, u32 sample_rate, u8 channel_cou
auto time_limit = AK::Duration::from_seconds(1);
auto start_time = MonotonicTime::now_coarse();
i64 last_sample = 0;
size_t sample_count = 0;
while (true) {
@@ -101,6 +102,9 @@ static inline void decode_audio(StringView path, u32 sample_rate, u8 channel_cou
EXPECT_EQ(block.sample_rate(), sample_rate);
EXPECT_EQ(block.channel_count(), channel_count);
VERIFY(sample_count == 0 || last_sample <= block.timestamp_in_samples());
last_sample = block.timestamp_in_samples() + static_cast<i64>(block.sample_count());
sample_count += block.sample_count();
}

View File

@@ -351,3 +351,6 @@ Text/input/wpt-import/html/semantics/popovers/popover-toggle-source.tentative.ht
; Flaky: https://github.com/LadybirdBrowser/ladybird/issues/5257
Text/input/selection-over-multiple-code-units.html
; Flaky: https://github.com/LadybirdBrowser/ladybird/issues/6846
Text/input/wpt-import/html/semantics/embedded-content/the-img-element/naturalWidth-naturalHeight-width-height.tentative.html

View File

@@ -2,10 +2,9 @@ Harness status: OK
Found 5 tests
1 Pass
4 Fail
5 Pass
Pass Property font-language-override value 'normal'
Fail Property font-language-override value '"KSW"'
Fail Property font-language-override value '"ENG "'
Fail Property font-language-override value '"en "'
Fail Property font-language-override value '" en "'
Pass Property font-language-override value '"KSW"'
Pass Property font-language-override value '"ENG "'
Pass Property font-language-override value '"en "'
Pass Property font-language-override value '" en "'

View File

@@ -2,14 +2,13 @@ Harness status: OK
Found 9 tests
2 Pass
7 Fail
9 Pass
Pass e.style['font-language-override'] = "normal" should set the property value
Fail e.style['font-language-override'] = "\"KSW\"" should set the property value
Pass e.style['font-language-override'] = "\"KSW\"" should set the property value
Pass e.style['font-language-override'] = "\"APPH\"" should set the property value
Fail e.style['font-language-override'] = "\"ENG \"" should set the property value
Fail e.style['font-language-override'] = "\"ksw\"" should set the property value
Fail e.style['font-language-override'] = "\"tr\"" should set the property value
Fail e.style['font-language-override'] = "\"en \"" should set the property value
Fail e.style['font-language-override'] = "\" en \"" should set the property value
Fail e.style['font-language-override'] = "\"1 %\"" should set the property value
Pass e.style['font-language-override'] = "\"ENG \"" should set the property value
Pass e.style['font-language-override'] = "\"ksw\"" should set the property value
Pass e.style['font-language-override'] = "\"tr\"" should set the property value
Pass e.style['font-language-override'] = "\"en \"" should set the property value
Pass e.style['font-language-override'] = "\" en \"" should set the property value
Pass e.style['font-language-override'] = "\"1 %\"" should set the property value

View File

@@ -0,0 +1,88 @@
Harness status: OK
Found 82 tests
68 Pass
14 Fail
Pass Interpolation between translateX(0px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(0px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateX(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between translateY(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between translateY(0%) and translateX(50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateX(50px) and translateY(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(50px) and translateY(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateX(50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateX(50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateZ(50px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateZ(50px) and translateX(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translateZ(-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translateZ(-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(0%) and translate(50%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(0%) and translate(50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(50%) and translate(100%, 50%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(50%) and translate(100%, 50%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(0%, 50%) and translate(50%, 100%) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(0%, 50%) and translate(50%, 100%) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate3d(0,0,-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate3d(0,0,-50px) and translateZ(50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(50px, 0px) and translate(100px, 0px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(50px, 0px) and translate(100px, 0px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between translate(50px, -50px) and translate(100px, 50px) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between translate(50px, -50px) and translate(100px, 50px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between rotate(30deg) and rotate(90deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between rotate(30deg) and rotate(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between rotateZ(30deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between rotateZ(30deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between rotate(0deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between rotate(0deg) and rotateZ(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between rotateX(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between rotateX(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between rotate(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between rotate(0deg) and rotateX(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1) and scale(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1) and scale(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1, 3) and scale(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1, 3) and scale(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleX(1) and scaleX(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleX(1) and scaleX(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleY(1) and scaleY(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleY(1) and scaleY(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleZ(1) and scaleZ(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleZ(1) and scaleZ(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleX(2) and scaleY(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleX(2) and scaleY(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleX(2) and scaleY(3) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleX(2) and scaleY(3) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scaleZ(1) and scale(2) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scaleZ(1) and scale(2) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1, 2) and scale(3, 4) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1, 2) and scale(3, 4) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale3d(1, 2, 3) and scale3d(4, 5, 6) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale3d(1, 2, 3) and scale3d(4, 5, 6) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale3d(1, 2, 3) and scale(4, 5) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale3d(1, 2, 3) and scale(4, 5) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between scale(1, 2) and scale3d(3, 4, 5) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between scale(1, 2) and scale3d(3, 4, 5) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skewX(0deg) and skewX(60deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skewX(0deg) and skewX(60deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skewX(0deg) and skewX(90deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skewX(0deg) and skewX(90deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skewX(0deg) and skewX(180deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skewX(0deg) and skewX(180deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skew(0deg, 0deg) and skew(60deg, 60deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skew(0deg, 0deg) and skew(60deg, 60deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between skew(45deg, 0deg) and skew(0deg, 45deg) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between skew(45deg, 0deg) and skew(0deg, 45deg) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between perspective(10px) and perspective(2.5px) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between perspective(10px) and perspective(2.5px) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between perspective(10px) and perspective(none) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between perspective(10px) and perspective(none) gives the correct computed value halfway according to computedStyleMap with zoom active.
Fail Interpolation between perspective(none) and perspective(none) gives the correct computed value halfway according to computedStyleMap.
Fail Interpolation between perspective(none) and perspective(none) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between matrix(2, 0, 0, 2, 10, 30) and matrix(4, 0, 0, 6, 14, 10) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between matrix(2, 0, 0, 2, 10, 30) and matrix(4, 0, 0, 6, 14, 10) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1) and matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1) and matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1) gives the correct computed value halfway according to computedStyleMap with zoom active.
Pass Interpolation between matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1) and matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1) gives the correct computed value halfway according to computedStyleMap.
Pass Interpolation between matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1) and matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1) gives the correct computed value halfway according to computedStyleMap with zoom active.

View File

@@ -2,5 +2,5 @@ Harness status: OK
Found 1 tests
1 Fail
Fail Serialization of <mf-name> : <mf-value> with custom property feature name and ident value
1 Pass
Pass Serialization of <mf-name> : <mf-value> with custom property feature name and ident value

View File

@@ -30,14 +30,22 @@
const sendMessageAndWait = (message) => {
return new Promise((resolve) => {
window.onmessage = ({ data }) => {
const listener = ({ data }) => {
window.removeEventListener('message', listener);
resolve(data);
};
window.addEventListener('message', listener);
testIframe.contentWindow.postMessage(message, "*");
});
};
// wait for the iframe to be ready
await new Promise((resolve) => {
testIframe.addEventListener('load', resolve);
if (testIframe.contentDocument.readyState === 'complete')
resolve();
});
const gamepad = internals.connectVirtualGamepad();
await handleSDLInputEvents();
listenForGamepadConnected();

View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<meta charset="UTF-8">
<title>transform interpolation</title>
<link rel="help" href="https://drafts.css-houdini.org/css-typed-om/#transformvalue-objects">
<meta name="assert" content="transform gives the correct computed values when interpolated">
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../web-animations/testcommon.js"></script>
<body>
<script>
function interpolation_test(from, to, expected_50) {
test(t => {
let div = createDiv(t);
let anim = div.animate({transform: [from, to]}, 2000);
anim.pause();
anim.currentTime = 1000;
let halfway = div.computedStyleMap().get('transform').toString();
assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
}, "Interpolation between " + from + " and " + to + " gives the correct " +
"computed value halfway according to computedStyleMap.");
test(t => {
let div = createDiv(t);
div.style.zoom = 1.25;
let anim = div.animate({transform: [from, to]}, 2000);
anim.pause();
anim.currentTime = 1000;
let halfway = div.computedStyleMap().get('transform').toString();
assert_equals(halfway, expected_50, "The value at 50% progress is as expected");
}, "Interpolation between " + from + " and " + to + " gives the correct " +
"computed value halfway according to computedStyleMap with zoom active.");
}
interpolation_test('translateX(0px)', 'translateX(50px)', 'translate(25px, 0px)');
interpolation_test('translateX(0%)', 'translateX(50%)', 'translate(25%, 0px)');
interpolation_test('translateY(0%)', 'translateX(50%)', 'translate(25%, 0px)');
interpolation_test('translateX(50px)', 'translateY(50px)', 'translate(25px, 25px)');
interpolation_test('translateX(50px)', 'translateZ(50px)', 'translate3d(25px, 0px, 25px)');
interpolation_test('translateZ(50px)', 'translateX(50px)', 'translate3d(25px, 0px, 25px)');
interpolation_test('translateZ(-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
interpolation_test('translate(0%)', 'translate(50%)', 'translate(25%, 0px)');
interpolation_test('translate(50%)', 'translate(100%, 50%)', 'translate(75%, 25%)');
interpolation_test('translate(0%, 50%)', 'translate(50%, 100%)', 'translate(25%, 75%)');
interpolation_test('translate3d(0,0,-50px)','translateZ(50px)', 'translate3d(0px, 0px, 0px)');
interpolation_test('translate(50px, 0px)', 'translate(100px, 0px)', 'translate(75px, 0px)');
interpolation_test('translate(50px, -50px)', 'translate(100px, 50px)', 'translate(75px, 0px)');
interpolation_test('rotate(30deg)', 'rotate(90deg)', 'rotate(60deg)');
interpolation_test('rotateZ(30deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 60deg)');
interpolation_test('rotate(0deg)', 'rotateZ(90deg)', 'rotate3d(0, 0, 1, 45deg)');
interpolation_test('rotateX(0deg)','rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
interpolation_test('rotate(0deg)', 'rotateX(90deg)', 'rotate3d(1, 0, 0, 45deg)');
interpolation_test('scale(1)', 'scale(2)', 'scale(1.5, 1.5)');
interpolation_test('scale(1, 3)', 'scale(2)', 'scale(1.5, 2.5)');
interpolation_test('scaleX(1)', 'scaleX(2)', 'scale(1.5, 1)');
interpolation_test('scaleY(1)', 'scaleY(2)', 'scale(1, 1.5)');
interpolation_test('scaleZ(1)', 'scaleZ(2)', 'scale3d(1, 1, 1.5)');
interpolation_test('scaleX(2)', 'scaleY(2)', 'scale(1.5, 1.5)');
interpolation_test('scaleX(2)', 'scaleY(3)', 'scale(1.5, 2)');
interpolation_test('scaleZ(1)', 'scale(2)', 'scale3d(1.5, 1.5, 1)');
interpolation_test('scale(1, 2)', 'scale(3, 4)', 'scale(2, 3)');
interpolation_test('scale3d(1, 2, 3)', 'scale3d(4, 5, 6)', 'scale3d(2.5, 3.5, 4.5)');
interpolation_test('scale3d(1, 2, 3)', 'scale(4, 5)', 'scale3d(2.5, 3.5, 2)');
interpolation_test('scale(1, 2)', 'scale3d(3, 4, 5)', 'scale3d(2, 3, 3)');
interpolation_test('skewX(0deg)', 'skewX(60deg)', 'skewX(30deg)');
interpolation_test('skewX(0deg)', 'skewX(90deg)', 'skewX(45deg)');
interpolation_test('skewX(0deg)', 'skewX(180deg)', 'skewX(90deg)');
interpolation_test('skew(0deg, 0deg)', 'skew(60deg, 60deg)', 'skew(30deg, 30deg)');
interpolation_test('skew(45deg, 0deg)', 'skew(0deg, 45deg)', 'skew(22.5deg, 22.5deg)');
interpolation_test('perspective(10px)', 'perspective(2.5px)', 'perspective(4px)');
interpolation_test('perspective(10px)', 'perspective(none)', 'perspective(20px)');
interpolation_test('perspective(none)', 'perspective(none)', 'perspective(none)');
// A matrix() with just scale and translation.
interpolation_test('matrix(2, 0, 0, 2, 10, 30)', 'matrix(4, 0, 0, 6, 14, 10)', 'matrix(3, 0, 0, 4, 12, 20)');
// A matrix3d() with just scale and translation.
interpolation_test('matrix3d(1, 0, 0, 0, 0, 4, 0, 0, 0, 0, 1, 0, 5, 10, 4, 1)', 'matrix3d(3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 3, 0, -11, 2, 2, 1)', 'matrix3d(2, 0, 0, 0, 0, 3, 0, 0, 0, 0, 2, 0, -3, 6, 3, 1)');
// A matrix3d() with just perspective.
interpolation_test('matrix3d(1, 0, 0, 3, 0, 1, 0, 2, 0, 0, 1, 8, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 5, 0, 1, 0, 8, 0, 0, 1, 14, 0, 0, 0, 1)', 'matrix3d(1, 0, 0, 4, 0, 1, 0, 5, 0, 0, 1, 11, 0, 0, 0, 1)');
// NOTE: New tests added here should also be added in
// transform-interpolation-inline-value.html.
</script>