mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-12-05 01:10:24 +00:00
Compare commits
30 Commits
ec14948175
...
fa262d2db5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa262d2db5 | ||
|
|
d5e5dbdf3d | ||
|
|
540bbae480 | ||
|
|
11b8bbeadf | ||
|
|
7d2f631d4c | ||
|
|
61f9b324c7 | ||
|
|
3083b19b09 | ||
|
|
740629a3a8 | ||
|
|
ba40da5e5f | ||
|
|
d7ebc4eef4 | ||
|
|
9547889a6b | ||
|
|
871f121c75 | ||
|
|
8a02161481 | ||
|
|
41eb7251e4 | ||
|
|
9394c9a10b | ||
|
|
15fa6676b0 | ||
|
|
e4dc2663ba | ||
|
|
911ecf1450 | ||
|
|
195a67ed80 | ||
|
|
5fb78ae455 | ||
|
|
04238d0f3f | ||
|
|
d8f5971ddf | ||
|
|
acce880359 | ||
|
|
1ae4429f3b | ||
|
|
eaece1d12c | ||
|
|
5dbb857c40 | ||
|
|
0664e9fbf5 | ||
|
|
ae5e200dfc | ||
|
|
7e325d64f5 | ||
|
|
a1358fa970 |
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
38
AK/Time.cpp
38
AK/Time.cpp
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = ¬ifier;
|
||||
notifier_data->m_notifier_type = notifier.type();
|
||||
notifier_data->wait_event.handle = event;
|
||||
NTSTATUS status = NtCreateWaitCompletionPacket(¬ifier_data->wait_packet.handle, GENERIC_READ | GENERIC_WRITE, NULL);
|
||||
NTSTATUS status = g_system.NtCreateWaitCompletionPacket(¬ifier_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(¬ifier, 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,10 +119,8 @@ WebIDL::ExceptionOr<Utf16String> CSSScale::to_string() const
|
||||
builder.append(TRY(m_x->to_string()));
|
||||
|
||||
// 3. If this’s 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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 we’re traversing the node tree. This is because the post-connection
|
||||
// steps can modify the tree’s 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 we’re traversing the node tree. This is because the post-connection steps can
|
||||
// modify the tree’s 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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <AK/Vector.h>
|
||||
#include <LibGfx/Matrix4x4.h>
|
||||
#include <LibWeb/Export.h>
|
||||
#include <LibWeb/Painting/Paintable.h>
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 "'
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user