diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp
index 59f4da8db2..e6a85865a3 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp
+++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.cpp
@@ -1836,4 +1836,26 @@ void HTMLMediaElement::reject_pending_play_promises(ReadonlySpan, Optional display_time)
+{
+ if (display_time.has_value() && !m_display_time.has_value()) {
+ if (potentially_playing()) {
+ m_tracking_mouse_position_while_playing = true;
+ on_paused();
+ }
+ } else if (!display_time.has_value() && m_display_time.has_value()) {
+ if (m_tracking_mouse_position_while_playing) {
+ m_tracking_mouse_position_while_playing = false;
+ on_playing();
+ }
+ }
+
+ m_display_time = move(display_time);
+}
+
+double HTMLMediaElement::layout_display_time(Badge) const
+{
+ return m_display_time.value_or(current_time());
+}
+
}
diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h
index e038a855e3..044d254f94 100644
--- a/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h
+++ b/Userland/Libraries/LibWeb/HTML/HTMLMediaElement.h
@@ -96,9 +96,19 @@ public:
JS::NonnullGCPtr audio_tracks() const { return *m_audio_tracks; }
JS::NonnullGCPtr video_tracks() const { return *m_video_tracks; }
+ enum class MouseTrackingComponent {
+ Timeline,
+ Volume,
+ };
+ void set_layout_mouse_tracking_component(Badge, Optional mouse_tracking_component) { m_mouse_tracking_component = move(mouse_tracking_component); }
+ Optional const& layout_mouse_tracking_component(Badge) const { return m_mouse_tracking_component; }
+
void set_layout_mouse_position(Badge, Optional mouse_position) { m_mouse_position = move(mouse_position); }
Optional const& layout_mouse_position(Badge) const { return m_mouse_position; }
+ void set_layout_display_time(Badge, Optional display_time);
+ double layout_display_time(Badge) const;
+
struct CachedLayoutBoxes {
Optional control_box_rect;
Optional playback_button_rect;
@@ -259,7 +269,10 @@ private:
bool m_seek_in_progress = false;
// Cached state for layout.
+ Optional m_mouse_tracking_component;
+ bool m_tracking_mouse_position_while_playing { false };
Optional m_mouse_position;
+ Optional m_display_time;
mutable CachedLayoutBoxes m_layout_boxes;
};
diff --git a/Userland/Libraries/LibWeb/Painting/MediaPaintable.cpp b/Userland/Libraries/LibWeb/Painting/MediaPaintable.cpp
index 1831edb349..437ae54c07 100644
--- a/Userland/Libraries/LibWeb/Painting/MediaPaintable.cpp
+++ b/Userland/Libraries/LibWeb/Painting/MediaPaintable.cpp
@@ -9,10 +9,11 @@
#include
#include
#include
+#include
#include
-#include
#include
#include
+#include
#include
namespace Web::Painting {
@@ -101,9 +102,9 @@ MediaPaintable::Components MediaPaintable::compute_control_bar_components(PaintC
remaining_rect.take_from_right(components.speaker_button_size + component_padding);
}
- auto current_time = human_readable_digital_time(round(media_element.current_time()));
+ auto display_time = human_readable_digital_time(round(media_element.layout_display_time({})));
auto duration = human_readable_digital_time(isnan(media_element.duration()) ? 0 : round(media_element.duration()));
- components.timestamp = String::formatted("{} / {}", current_time, duration).release_value_but_fixme_should_propagate_errors();
+ components.timestamp = String::formatted("{} / {}", display_time, duration).release_value_but_fixme_should_propagate_errors();
auto const& scaled_font = layout_node().scaled_font(context);
components.timestamp_font = scaled_font.with_size(10);
@@ -138,7 +139,7 @@ void MediaPaintable::paint_control_bar_playback_button(PaintContext& context, HT
auto playback_button_offset_y = (components.playback_button_rect.height() - playback_button_size) / 2;
auto playback_button_location = components.playback_button_rect.top_left().translated(playback_button_offset_x, playback_button_offset_y);
- auto playback_button_is_hovered = mouse_position.has_value() && components.playback_button_rect.contains(*mouse_position);
+ auto playback_button_is_hovered = rect_is_hovered(media_element, components.playback_button_rect, mouse_position);
auto playback_button_color = control_button_color(playback_button_is_hovered);
if (media_element.paused()) {
@@ -172,7 +173,7 @@ void MediaPaintable::paint_control_bar_timeline(PaintContext& context, HTML::HTM
auto timelime_scrub_rect = components.timeline_rect;
timelime_scrub_rect.shrink(components.timeline_button_size, timelime_scrub_rect.height() - components.timeline_button_size / 2);
- auto playback_percentage = isnan(media_element.duration()) ? 0.0 : media_element.current_time() / media_element.duration();
+ auto playback_percentage = isnan(media_element.duration()) ? 0.0 : media_element.layout_display_time({}) / media_element.duration();
auto playback_position = static_cast(static_cast(timelime_scrub_rect.width())) * playback_percentage;
auto timeline_button_offset_x = static_cast(round(playback_position));
@@ -190,7 +191,7 @@ void MediaPaintable::paint_control_bar_timeline(PaintContext& context, HTML::HTM
timeline_button_rect.shrink(timelime_scrub_rect.width() - components.timeline_button_size, timelime_scrub_rect.height() - components.timeline_button_size);
timeline_button_rect.set_x(timelime_scrub_rect.x() + timeline_button_offset_x - components.timeline_button_size / 2);
- auto timeline_is_hovered = mouse_position.has_value() && components.timeline_rect.contains(*mouse_position);
+ auto timeline_is_hovered = rect_is_hovered(media_element, components.timeline_rect, mouse_position, HTML::HTMLMediaElement::MouseTrackingComponent::Timeline);
auto timeline_color = control_button_color(timeline_is_hovered);
painter.fill_ellipse(timeline_button_rect.to_type(), timeline_color);
}
@@ -220,7 +221,7 @@ void MediaPaintable::paint_control_bar_speaker(PaintContext& context, HTML::HTML
return position.to_type().to_type();
};
- auto speaker_button_is_hovered = mouse_position.has_value() && components.speaker_button_rect.contains(*mouse_position);
+ auto speaker_button_is_hovered = rect_is_hovered(media_element, components.speaker_button_rect, mouse_position);
auto speaker_button_color = control_button_color(speaker_button_is_hovered);
Gfx::AntiAliasingPainter painter { context.painter() };
@@ -274,12 +275,12 @@ void MediaPaintable::paint_control_bar_volume(PaintContext& context, HTML::HTMLM
volume_button_rect.shrink(volume_scrub_rect.width() - components.volume_button_size, volume_scrub_rect.height() - components.volume_button_size);
volume_button_rect.set_x(volume_scrub_rect.x() + volume_button_offset_x - components.volume_button_size / 2);
- auto volume_is_hovered = mouse_position.has_value() && components.volume_rect.contains(*mouse_position);
+ auto volume_is_hovered = rect_is_hovered(media_element, components.volume_rect, mouse_position, HTML::HTMLMediaElement::MouseTrackingComponent::Volume);
auto volume_color = control_button_color(volume_is_hovered);
painter.fill_ellipse(volume_button_rect.to_type(), volume_color);
}
-MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badge, CSSPixelPoint position, unsigned button, unsigned)
+MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mousedown(Badge, CSSPixelPoint position, unsigned button, unsigned)
{
if (button != GUI::MouseButton::Primary)
return DispatchEventOfSameName::Yes;
@@ -287,6 +288,39 @@ MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badge(layout_box().dom_node());
auto const& cached_layout_boxes = media_element.cached_layout_boxes({});
+ if (cached_layout_boxes.timeline_rect.has_value() && cached_layout_boxes.timeline_rect->contains(position))
+ media_element.set_layout_mouse_tracking_component({}, HTML::HTMLMediaElement::MouseTrackingComponent::Timeline);
+ else if (cached_layout_boxes.volume_rect.has_value() && cached_layout_boxes.volume_rect->contains(position))
+ media_element.set_layout_mouse_tracking_component({}, HTML::HTMLMediaElement::MouseTrackingComponent::Volume);
+
+ if (media_element.layout_mouse_tracking_component({}).has_value())
+ const_cast(browsing_context()).event_handler().set_mouse_event_tracking_layout_node(&layout_node());
+
+ return DispatchEventOfSameName::Yes;
+}
+
+MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badge, CSSPixelPoint position, unsigned button, unsigned)
+{
+ auto& media_element = *verify_cast(layout_box().dom_node());
+ auto const& cached_layout_boxes = media_element.cached_layout_boxes({});
+
+ auto was_tracking_mouse = media_element.layout_mouse_tracking_component({}).has_value();
+ auto was_tracking_timeline = media_element.layout_mouse_tracking_component({}) == HTML::HTMLMediaElement::MouseTrackingComponent::Timeline;
+ media_element.set_layout_mouse_tracking_component({}, {});
+
+ if (was_tracking_mouse) {
+ if (was_tracking_timeline) {
+ set_current_time(media_element, *cached_layout_boxes.timeline_rect, position, Temporary::No);
+ media_element.set_layout_display_time({}, {});
+ }
+
+ const_cast(browsing_context()).event_handler().set_mouse_event_tracking_layout_node(nullptr);
+ return DispatchEventOfSameName::Yes;
+ }
+
+ if (button != GUI::MouseButton::Primary)
+ return DispatchEventOfSameName::Yes;
+
// FIXME: This runs from outside the context of any user script, so we do not have a running execution
// context. This pushes one to allow the promise creation hook to run.
auto& environment_settings = document().relevant_settings_object();
@@ -309,12 +343,7 @@ MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badgecontains(position)) {
- auto x_offset = position.x() - cached_layout_boxes.timeline_rect->x();
- auto x_percentage = static_cast(x_offset) / static_cast(cached_layout_boxes.timeline_rect->width());
-
- auto position = static_cast(x_percentage) * media_element.duration();
- media_element.set_current_time(position);
-
+ set_current_time(media_element, *cached_layout_boxes.timeline_rect, position, Temporary::No);
return DispatchEventOfSameName::Yes;
}
@@ -324,11 +353,7 @@ MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badgecontains(position)) {
- auto x_offset = position.x() - cached_layout_boxes.volume_rect->x();
- auto volume = static_cast(x_offset) / static_cast(cached_layout_boxes.volume_rect->width());
-
- media_element.set_volume(volume).release_value_but_fixme_should_propagate_errors();
-
+ set_volume(media_element, *cached_layout_boxes.volume_rect, position);
return DispatchEventOfSameName::Yes;
}
@@ -342,6 +367,21 @@ MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badge, CSSPixelPoint position, unsigned, unsigned)
{
auto& media_element = *verify_cast(layout_box().dom_node());
+ auto const& cached_layout_boxes = media_element.cached_layout_boxes({});
+
+ if (auto const& mouse_tracking_component = media_element.layout_mouse_tracking_component({}); mouse_tracking_component.has_value()) {
+ switch (*mouse_tracking_component) {
+ case HTML::HTMLMediaElement::MouseTrackingComponent::Timeline:
+ if (cached_layout_boxes.timeline_rect.has_value())
+ set_current_time(media_element, *cached_layout_boxes.timeline_rect, position, Temporary::Yes);
+ break;
+
+ case HTML::HTMLMediaElement::MouseTrackingComponent::Volume:
+ if (cached_layout_boxes.volume_rect.has_value())
+ set_volume(media_element, *cached_layout_boxes.volume_rect, position);
+ break;
+ }
+ }
if (absolute_rect().contains(position)) {
media_element.set_layout_mouse_position({}, position);
@@ -352,4 +392,44 @@ MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mousemove(Badge(x_offset) / static_cast(timeline_rect.width());
+ auto position = static_cast(x_percentage) * media_element.duration();
+
+ switch (temporarily) {
+ case Temporary::Yes:
+ media_element.set_layout_display_time({}, position);
+ break;
+ case Temporary::No:
+ media_element.set_current_time(position);
+ break;
+ }
+}
+
+void MediaPaintable::set_volume(HTML::HTMLMediaElement& media_element, CSSPixelRect volume_rect, CSSPixelPoint mouse_position)
+{
+ auto x_offset = mouse_position.x() - volume_rect.x();
+ x_offset = max(x_offset, 0);
+ x_offset = min(x_offset, volume_rect.width());
+
+ auto volume = static_cast(x_offset) / static_cast(volume_rect.width());
+ media_element.set_volume(volume).release_value_but_fixme_should_propagate_errors();
+}
+
+bool MediaPaintable::rect_is_hovered(HTML::HTMLMediaElement const& media_element, Optional const& rect, Optional const& mouse_position, Optional const& allowed_mouse_tracking_component)
+{
+ if (auto const& mouse_tracking_component = media_element.layout_mouse_tracking_component({}); mouse_tracking_component.has_value())
+ return mouse_tracking_component == allowed_mouse_tracking_component;
+
+ if (!rect.has_value() || !mouse_position.has_value())
+ return false;
+
+ return rect->contains(*mouse_position);
+}
+
}
diff --git a/Userland/Libraries/LibWeb/Painting/MediaPaintable.h b/Userland/Libraries/LibWeb/Painting/MediaPaintable.h
index e6c0cf4ee3..9337f6ffa0 100644
--- a/Userland/Libraries/LibWeb/Painting/MediaPaintable.h
+++ b/Userland/Libraries/LibWeb/Painting/MediaPaintable.h
@@ -7,6 +7,7 @@
#pragma once
#include
+#include
#include
#include
@@ -43,6 +44,7 @@ private:
};
virtual bool wants_mouse_events() const override { return true; }
+ virtual DispatchEventOfSameName handle_mousedown(Badge, CSSPixelPoint, unsigned button, unsigned modifiers) override;
virtual DispatchEventOfSameName handle_mouseup(Badge, CSSPixelPoint, unsigned button, unsigned modifiers) override;
virtual DispatchEventOfSameName handle_mousemove(Badge, CSSPixelPoint, unsigned buttons, unsigned modifiers) override;
@@ -52,6 +54,15 @@ private:
static void paint_control_bar_timestamp(PaintContext&, Components const&);
static void paint_control_bar_speaker(PaintContext&, HTML::HTMLMediaElement const&, Components const& components, Optional const& mouse_position);
static void paint_control_bar_volume(PaintContext&, HTML::HTMLMediaElement const&, Components const&, Optional const& mouse_position);
+
+ enum class Temporary {
+ Yes,
+ No,
+ };
+ static void set_current_time(HTML::HTMLMediaElement& media_element, CSSPixelRect timeline_rect, CSSPixelPoint mouse_position, Temporary);
+ static void set_volume(HTML::HTMLMediaElement& media_element, CSSPixelRect volume_rect, CSSPixelPoint mouse_position);
+
+ static bool rect_is_hovered(HTML::HTMLMediaElement const& media_element, Optional const& rect, Optional const& mouse_position, Optional const& allowed_mouse_tracking_component = {});
};
}