MediaPaintable.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. /*
  2. * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/Array.h>
  7. #include <AK/NumberFormat.h>
  8. #include <LibGUI/Event.h>
  9. #include <LibGfx/AntiAliasingPainter.h>
  10. #include <LibWeb/DOM/Document.h>
  11. #include <LibWeb/HTML/HTMLAudioElement.h>
  12. #include <LibWeb/HTML/HTMLMediaElement.h>
  13. #include <LibWeb/HTML/HTMLVideoElement.h>
  14. #include <LibWeb/Layout/ReplacedBox.h>
  15. #include <LibWeb/Painting/MediaPaintable.h>
  16. namespace Web::Painting {
  17. static constexpr auto control_box_color = Gfx::Color::from_rgb(0x26'26'26);
  18. static constexpr auto control_highlight_color = Gfx::Color::from_rgb(0x1d'99'f3);
  19. static constexpr Gfx::Color control_button_color(bool is_hovered)
  20. {
  21. if (!is_hovered)
  22. return Color::White;
  23. return control_highlight_color;
  24. }
  25. MediaPaintable::MediaPaintable(Layout::ReplacedBox const& layout_box)
  26. : PaintableBox(layout_box)
  27. {
  28. }
  29. Optional<DevicePixelPoint> MediaPaintable::mouse_position(PaintContext& context, HTML::HTMLMediaElement const& media_element)
  30. {
  31. auto const& layout_mouse_position = media_element.layout_mouse_position({});
  32. if (layout_mouse_position.has_value() && media_element.document().hovered_node() == &media_element)
  33. return context.rounded_device_point(*layout_mouse_position);
  34. return {};
  35. }
  36. void MediaPaintable::fill_triangle(Gfx::Painter& painter, Gfx::IntPoint location, Array<Gfx::IntPoint, 3> coordinates, Color color)
  37. {
  38. Gfx::AntiAliasingPainter aa_painter { painter };
  39. Gfx::Path path;
  40. path.move_to((coordinates[0] + location).to_type<float>());
  41. path.line_to((coordinates[1] + location).to_type<float>());
  42. path.line_to((coordinates[2] + location).to_type<float>());
  43. path.close();
  44. aa_painter.fill_path(path, color, Gfx::Painter::WindingRule::EvenOdd);
  45. }
  46. void MediaPaintable::paint_media_controls(PaintContext& context, HTML::HTMLMediaElement const& media_element, DevicePixelRect media_rect, Optional<DevicePixelPoint> const& mouse_position) const
  47. {
  48. auto maximum_control_box_size = context.rounded_device_pixels(40);
  49. auto playback_padding = context.rounded_device_pixels(5);
  50. auto control_box_rect = media_rect;
  51. if (control_box_rect.height() > maximum_control_box_size)
  52. control_box_rect.take_from_top(control_box_rect.height() - maximum_control_box_size);
  53. context.painter().fill_rect(control_box_rect.to_type<int>(), control_box_color.with_alpha(0xd0));
  54. media_element.cached_layout_boxes({}).control_box_rect = context.scale_to_css_rect(control_box_rect);
  55. control_box_rect = paint_control_bar_playback_button(context, media_element, control_box_rect, mouse_position);
  56. control_box_rect.take_from_left(playback_padding);
  57. control_box_rect = paint_control_bar_timeline(context, media_element, control_box_rect, mouse_position);
  58. control_box_rect.take_from_left(playback_padding);
  59. control_box_rect = paint_control_bar_timestamp(context, media_element, control_box_rect);
  60. control_box_rect.take_from_left(playback_padding);
  61. }
  62. DevicePixelRect MediaPaintable::paint_control_bar_playback_button(PaintContext& context, HTML::HTMLMediaElement const& media_element, DevicePixelRect control_box_rect, Optional<DevicePixelPoint> const& mouse_position) const
  63. {
  64. auto maximum_playback_button_size = context.rounded_device_pixels(15);
  65. auto maximum_playback_button_offset_x = context.rounded_device_pixels(15);
  66. auto playback_button_size = min(maximum_playback_button_size, control_box_rect.height() / 2);
  67. auto playback_button_offset_x = min(maximum_playback_button_offset_x, control_box_rect.width());
  68. auto playback_button_offset_y = (control_box_rect.height() - playback_button_size) / 2;
  69. auto playback_button_location = control_box_rect.top_left().translated(playback_button_offset_x, playback_button_offset_y);
  70. auto playback_button_hover_rect = DevicePixelRect {
  71. control_box_rect.top_left(),
  72. { playback_button_size + playback_button_offset_x * 2, control_box_rect.height() }
  73. };
  74. media_element.cached_layout_boxes({}).playback_button_rect = context.scale_to_css_rect(playback_button_hover_rect);
  75. auto playback_button_is_hovered = mouse_position.has_value() && playback_button_hover_rect.contains(*mouse_position);
  76. auto playback_button_color = control_button_color(playback_button_is_hovered);
  77. if (media_element.paused()) {
  78. Array<Gfx::IntPoint, 3> play_button_coordinates { {
  79. { 0, 0 },
  80. { static_cast<int>(playback_button_size), static_cast<int>(playback_button_size) / 2 },
  81. { 0, static_cast<int>(playback_button_size) },
  82. } };
  83. fill_triangle(context.painter(), playback_button_location.to_type<int>(), play_button_coordinates, playback_button_color);
  84. } else {
  85. DevicePixelRect pause_button_left_rect {
  86. playback_button_location,
  87. { maximum_playback_button_size / 3, playback_button_size }
  88. };
  89. DevicePixelRect pause_button_right_rect {
  90. playback_button_location.translated(maximum_playback_button_size * 2 / 3, 0),
  91. { maximum_playback_button_size / 3, playback_button_size }
  92. };
  93. context.painter().fill_rect(pause_button_left_rect.to_type<int>(), playback_button_color);
  94. context.painter().fill_rect(pause_button_right_rect.to_type<int>(), playback_button_color);
  95. }
  96. control_box_rect.take_from_left(playback_button_hover_rect.width());
  97. return control_box_rect;
  98. }
  99. DevicePixelRect MediaPaintable::paint_control_bar_timeline(PaintContext& context, HTML::HTMLMediaElement const& media_element, DevicePixelRect control_box_rect, Optional<DevicePixelPoint> const& mouse_position) const
  100. {
  101. auto maximum_timeline_button_size = context.rounded_device_pixels(16);
  102. auto timeline_rect = control_box_rect;
  103. if (is<HTML::HTMLAudioElement>(media_element))
  104. timeline_rect.set_width(min(control_box_rect.width() * 6 / 10, timeline_rect.width() * 4 / 10));
  105. else
  106. timeline_rect.set_width(min(control_box_rect.width() * 6 / 10, timeline_rect.width()));
  107. media_element.cached_layout_boxes({}).timeline_rect = context.scale_to_css_rect(timeline_rect);
  108. auto playback_percentage = media_element.current_time() / media_element.duration();
  109. auto playback_position = static_cast<double>(static_cast<int>(timeline_rect.width())) * playback_percentage;
  110. auto timeline_button_size = min(maximum_timeline_button_size, timeline_rect.height() / 2);
  111. auto timeline_button_offset_x = static_cast<DevicePixels>(round(playback_position));
  112. Gfx::AntiAliasingPainter painter { context.painter() };
  113. auto playback_timelime_scrub_rect = timeline_rect;
  114. playback_timelime_scrub_rect.shrink(0, timeline_rect.height() - timeline_button_size / 2);
  115. auto timeline_past_rect = playback_timelime_scrub_rect;
  116. timeline_past_rect.set_width(timeline_button_offset_x);
  117. painter.fill_rect_with_rounded_corners(timeline_past_rect.to_type<int>(), control_highlight_color.lightened(), 4);
  118. auto timeline_future_rect = playback_timelime_scrub_rect;
  119. timeline_future_rect.take_from_left(timeline_button_offset_x);
  120. painter.fill_rect_with_rounded_corners(timeline_future_rect.to_type<int>(), Color::Black, 4);
  121. auto timeline_button_rect = timeline_rect;
  122. timeline_button_rect.shrink(timeline_rect.width() - timeline_button_size, timeline_rect.height() - timeline_button_size);
  123. timeline_button_rect.set_x(timeline_rect.x() + timeline_button_offset_x - timeline_button_size / 2);
  124. auto timeline_is_hovered = mouse_position.has_value() && timeline_rect.contains(*mouse_position);
  125. auto timeline_color = control_button_color(timeline_is_hovered);
  126. painter.fill_ellipse(timeline_button_rect.to_type<int>(), timeline_color);
  127. control_box_rect.take_from_left(timeline_rect.width() + timeline_button_size / 2);
  128. return control_box_rect;
  129. }
  130. DevicePixelRect MediaPaintable::paint_control_bar_timestamp(PaintContext& context, HTML::HTMLMediaElement const& media_element, DevicePixelRect control_box_rect) const
  131. {
  132. auto current_time = human_readable_digital_time(round(media_element.current_time()));
  133. auto duration = human_readable_digital_time(round(media_element.duration()));
  134. auto timestamp = String::formatted("{} / {}", current_time, duration).release_value_but_fixme_should_propagate_errors();
  135. auto timestamp_size = static_cast<DevicePixels::Type>(ceilf(context.painter().font().width(timestamp)));
  136. if (timestamp_size > control_box_rect.width())
  137. return control_box_rect;
  138. auto timestamp_rect = control_box_rect;
  139. timestamp_rect.set_width(timestamp_size);
  140. auto const& scaled_font = layout_node().scaled_font(context);
  141. context.painter().draw_text(timestamp_rect.to_type<int>(), timestamp, scaled_font, Gfx::TextAlignment::CenterLeft, Color::White);
  142. control_box_rect.take_from_left(timestamp_rect.width());
  143. return control_box_rect;
  144. }
  145. MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mouseup(Badge<EventHandler>, CSSPixelPoint position, unsigned button, unsigned)
  146. {
  147. if (button != GUI::MouseButton::Primary)
  148. return DispatchEventOfSameName::Yes;
  149. auto& media_element = *verify_cast<HTML::HTMLMediaElement>(layout_box().dom_node());
  150. auto const& cached_layout_boxes = media_element.cached_layout_boxes({});
  151. // FIXME: This runs from outside the context of any user script, so we do not have a running execution
  152. // context. This pushes one to allow the promise creation hook to run.
  153. auto& environment_settings = document().relevant_settings_object();
  154. environment_settings.prepare_to_run_script();
  155. ScopeGuard guard { [&] { environment_settings.clean_up_after_running_script(); } };
  156. auto toggle_playback = [&]() -> WebIDL::ExceptionOr<void> {
  157. if (media_element.paused())
  158. TRY(media_element.play());
  159. else
  160. TRY(media_element.pause());
  161. return {};
  162. };
  163. if (cached_layout_boxes.control_box_rect.has_value() && cached_layout_boxes.control_box_rect->contains(position)) {
  164. if (cached_layout_boxes.playback_button_rect.has_value() && cached_layout_boxes.playback_button_rect->contains(position)) {
  165. toggle_playback().release_value_but_fixme_should_propagate_errors();
  166. return DispatchEventOfSameName::Yes;
  167. }
  168. if (cached_layout_boxes.timeline_rect.has_value() && cached_layout_boxes.timeline_rect->contains(position)) {
  169. auto x_offset = position.x() - cached_layout_boxes.timeline_rect->x();
  170. auto x_percentage = static_cast<double>(x_offset) / static_cast<double>(cached_layout_boxes.timeline_rect->width());
  171. auto position = static_cast<double>(x_percentage) * media_element.duration();
  172. media_element.set_current_time(position);
  173. return DispatchEventOfSameName::Yes;
  174. }
  175. return DispatchEventOfSameName::No;
  176. }
  177. toggle_playback().release_value_but_fixme_should_propagate_errors();
  178. return DispatchEventOfSameName::Yes;
  179. }
  180. MediaPaintable::DispatchEventOfSameName MediaPaintable::handle_mousemove(Badge<EventHandler>, CSSPixelPoint position, unsigned, unsigned)
  181. {
  182. auto& media_element = *verify_cast<HTML::HTMLMediaElement>(layout_box().dom_node());
  183. if (absolute_rect().contains(position)) {
  184. media_element.set_layout_mouse_position({}, position);
  185. return DispatchEventOfSameName::Yes;
  186. }
  187. media_element.set_layout_mouse_position({}, {});
  188. return DispatchEventOfSameName::No;
  189. }
  190. }