blob: 72835ffbbee28ebc559edc9a79dff99ef9d4eddb [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/ozone/platform/wayland/host/wayland_window.h"
#include <algorithm>
#include <memory>
#include "base/bind.h"
#include "build/chromeos_buildflags.h"
#include "ui/base/cursor/ozone/bitmap_cursor_factory_ozone.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/events/ozone/events_ozone.h"
#include "ui/events/platform/platform_event_source.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/ozone/common/features.h"
#include "ui/ozone/platform/wayland/common/wayland_util.h"
#include "ui/ozone/platform/wayland/host/wayland_buffer_manager_host.h"
#include "ui/ozone/platform/wayland/host/wayland_connection.h"
#include "ui/ozone/platform/wayland/host/wayland_cursor_position.h"
#include "ui/ozone/platform/wayland/host/wayland_output_manager.h"
#include "ui/ozone/platform/wayland/host/wayland_pointer.h"
#include "ui/ozone/platform/wayland/host/wayland_subsurface.h"
#include "ui/ozone/platform/wayland/host/wayland_zcr_cursor_shapes.h"
#include "ui/ozone/public/mojom/wayland/wayland_overlay_config.mojom.h"
namespace {
bool OverlayStackOrderCompare(
const ui::ozone::mojom::WaylandOverlayConfigPtr& i,
const ui::ozone::mojom::WaylandOverlayConfigPtr& j) {
return i->z_order < j->z_order;
}
} // namespace
namespace ui {
WaylandWindow::WaylandWindow(PlatformWindowDelegate* delegate,
WaylandConnection* connection)
: delegate_(delegate),
connection_(connection),
wayland_overlay_delegation_enabled_(IsWaylandOverlayDelegationEnabled()),
accelerated_widget_(
connection->wayland_window_manager()->AllocateAcceleratedWidget()) {}
WaylandWindow::~WaylandWindow() {
shutting_down_ = true;
PlatformEventSource::GetInstance()->RemovePlatformEventDispatcher(this);
if (wayland_overlay_delegation_enabled_) {
connection_->wayland_window_manager()->RemoveSubsurface(
GetWidget(), primary_subsurface_.get());
}
for (const auto& widget_subsurface : wayland_subsurfaces()) {
connection_->wayland_window_manager()->RemoveSubsurface(
GetWidget(), widget_subsurface.get());
}
if (root_surface_)
connection_->wayland_window_manager()->RemoveWindow(GetWidget());
if (parent_window_)
parent_window_->set_child_window(nullptr);
}
void WaylandWindow::OnWindowLostCapture() {
delegate_->OnLostCapture();
}
void WaylandWindow::UpdateBufferScale(bool update_bounds) {
DCHECK(connection_->wayland_output_manager());
const auto* screen = connection_->wayland_output_manager()->wayland_screen();
// The client might not create screen at all.
if (!screen)
return;
const auto widget = GetWidget();
int32_t new_scale = 0;
if (parent_window_) {
new_scale = parent_window_->buffer_scale();
ui_scale_ = parent_window_->ui_scale_;
} else {
const auto display = (widget == gfx::kNullAcceleratedWidget)
? screen->GetPrimaryDisplay()
: screen->GetDisplayForAcceleratedWidget(widget);
new_scale = connection_->wayland_output_manager()
->GetOutput(display.id())
->scale_factor();
if (display::Display::HasForceDeviceScaleFactor())
ui_scale_ = display::Display::GetForcedDeviceScaleFactor();
else
ui_scale_ = display.device_scale_factor();
}
int32_t old_scale = buffer_scale();
root_surface_->SetBufferScale(new_scale, update_bounds);
// We need to keep DIP size of the window the same whenever the scale changes.
if (update_bounds)
SetBoundsDip(gfx::ScaleToRoundedRect(bounds_px_, 1.0 / old_scale));
}
gfx::AcceleratedWidget WaylandWindow::GetWidget() const {
return accelerated_widget_;
}
void WaylandWindow::SetPointerFocus(bool focus) {
has_pointer_focus_ = focus;
// Whenever the window gets the pointer focus back, we must reinitialize the
// cursor. Otherwise, it is invalidated whenever the pointer leaves the
// surface and is not restored by the Wayland compositor.
if (has_pointer_focus_ && bitmap_) {
// Translate physical pixels to DIPs.
gfx::Point hotspot_in_dips =
gfx::ScaleToRoundedPoint(bitmap_->hotspot(), 1.0f / ui_scale_);
connection_->SetCursorBitmap(bitmap_->bitmaps(), hotspot_in_dips,
buffer_scale());
}
}
void WaylandWindow::Show(bool inactive) {
if (background_buffer_id_ != 0u)
should_attach_background_buffer_ = true;
}
void WaylandWindow::Hide() {
NOTREACHED();
}
void WaylandWindow::Close() {
delegate_->OnClosed();
}
bool WaylandWindow::IsVisible() const {
NOTREACHED();
return false;
}
void WaylandWindow::PrepareForShutdown() {}
void WaylandWindow::SetBounds(const gfx::Rect& bounds_px) {
if (bounds_px_ == bounds_px)
return;
bounds_px_ = bounds_px;
root_surface_->SetOpaqueRegion(gfx::Rect(bounds_px_.size()));
delegate_->OnBoundsChanged(bounds_px_);
}
gfx::Rect WaylandWindow::GetBounds() const {
return bounds_px_;
}
void WaylandWindow::SetTitle(const base::string16& title) {}
void WaylandWindow::SetCapture() {
// Wayland doesn't allow explicit grabs. Instead, it sends events to "entered"
// windows. That is, if user enters their mouse pointer to a window, that
// window starts to receive events. However, Chromium may want to reroute
// these events to another window. In this case, tell the window manager that
// this specific window has grabbed the events, and they will be rerouted in
// WaylandWindow::DispatchEvent method.
if (!HasCapture())
connection_->wayland_window_manager()->GrabLocatedEvents(this);
}
void WaylandWindow::ReleaseCapture() {
if (HasCapture())
connection_->wayland_window_manager()->UngrabLocatedEvents(this);
// See comment in SetCapture() for details on wayland and grabs.
}
bool WaylandWindow::HasCapture() const {
return connection_->wayland_window_manager()->located_events_grabber() ==
this;
}
void WaylandWindow::ToggleFullscreen() {}
void WaylandWindow::Maximize() {}
void WaylandWindow::Minimize() {}
void WaylandWindow::Restore() {}
PlatformWindowState WaylandWindow::GetPlatformWindowState() const {
// Remove normal state for all the other types of windows as it's only the
// WaylandToplevelWindow that supports state changes.
return PlatformWindowState::kNormal;
}
void WaylandWindow::Activate() {
NOTIMPLEMENTED_LOG_ONCE();
}
void WaylandWindow::Deactivate() {
NOTIMPLEMENTED_LOG_ONCE();
}
void WaylandWindow::SetUseNativeFrame(bool use_native_frame) {
// Do nothing here since only shell surfaces can handle server-side
// decoration.
}
bool WaylandWindow::ShouldUseNativeFrame() const {
// Always returns false here since only shell surfaces can handle server-side
// decoration.
return false;
}
void WaylandWindow::SetCursor(PlatformCursor cursor) {
scoped_refptr<BitmapCursorOzone> bitmap =
BitmapCursorFactoryOzone::GetBitmapCursor(cursor);
if (bitmap_ == bitmap)
return;
bitmap_ = bitmap;
if (!bitmap_) {
// Hide the cursor.
connection_->SetCursorBitmap(std::vector<SkBitmap>(), gfx::Point(),
buffer_scale());
return;
}
// Check for Wayland server-side cursor support (e.g. exo for lacros).
if (connection_->zcr_cursor_shapes()) {
base::Optional<int32_t> shape =
WaylandZcrCursorShapes::ShapeFromType(bitmap->type());
// If the server supports this cursor type, use a server-side cursor.
if (shape.has_value()) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Lacros should not load image assets for default cursors. See
// BitmapCursorFactoryOzone::GetDefaultCursor().
DCHECK(bitmap_->bitmaps().empty());
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
connection_->zcr_cursor_shapes()->SetCursorShape(shape.value());
return;
}
// Fall through to client-side bitmap cursors.
}
// Translate physical pixels to DIPs.
gfx::Point hotspot_in_dips =
gfx::ScaleToRoundedPoint(bitmap_->hotspot(), 1.0f / ui_scale_);
connection_->SetCursorBitmap(bitmap_->bitmaps(), hotspot_in_dips,
buffer_scale());
}
void WaylandWindow::MoveCursorTo(const gfx::Point& location) {
NOTIMPLEMENTED();
}
void WaylandWindow::ConfineCursorToBounds(const gfx::Rect& bounds) {
NOTIMPLEMENTED();
}
void WaylandWindow::SetRestoredBoundsInPixels(const gfx::Rect& bounds_px) {
restored_bounds_px_ = bounds_px;
}
gfx::Rect WaylandWindow::GetRestoredBoundsInPixels() const {
return restored_bounds_px_;
}
bool WaylandWindow::ShouldWindowContentsBeTransparent() const {
NOTIMPLEMENTED_LOG_ONCE();
return false;
}
void WaylandWindow::SetAspectRatio(const gfx::SizeF& aspect_ratio) {
NOTIMPLEMENTED_LOG_ONCE();
}
void WaylandWindow::SetWindowIcons(const gfx::ImageSkia& window_icon,
const gfx::ImageSkia& app_icon) {
NOTIMPLEMENTED_LOG_ONCE();
}
void WaylandWindow::SizeConstraintsChanged() {}
bool WaylandWindow::CanDispatchEvent(const PlatformEvent& event) {
if (event->IsMouseEvent())
return has_pointer_focus_;
if (event->IsKeyEvent())
return has_keyboard_focus_;
if (event->IsTouchEvent())
return has_touch_focus_;
if (event->IsScrollEvent())
return has_pointer_focus_;
return false;
}
uint32_t WaylandWindow::DispatchEvent(const PlatformEvent& native_event) {
Event* event = static_cast<Event*>(native_event);
if (event->IsLocatedEvent()) {
auto* event_grabber =
connection_->wayland_window_manager()->located_events_grabber();
auto* root_parent_window = GetRootParentWindow();
// Wayland sends locations in DIP so they need to be translated to
// physical pixels.
event->AsLocatedEvent()->set_location_f(gfx::ScalePoint(
event->AsLocatedEvent()->location_f(), buffer_scale(), buffer_scale()));
// We must reroute the events to the event grabber iff these windows belong
// to the same root parent window. For example, there are 2 top level
// Wayland windows. One of them (window_1) has a child menu window that is
// the event grabber. If the mouse is moved over the window_1, it must
// reroute the events to the event grabber. If the mouse is moved over the
// window_2, the events mustn't be rerouted, because that belongs to another
// stack of windows. Remember that Wayland sends local surface coordinates,
// and continuing rerouting all the events may result in events sent to the
// grabber even though the mouse is over another root window.
//
if (event_grabber &&
root_parent_window == event_grabber->GetRootParentWindow()) {
ConvertEventLocationToTargetWindowLocation(
event_grabber->GetBounds().origin(), GetBounds().origin(),
event->AsLocatedEvent());
return event_grabber->DispatchEventToDelegate(native_event);
}
}
// Dispatch all keyboard events to the root window.
if (event->IsKeyEvent())
return GetRootParentWindow()->DispatchEventToDelegate(event);
return DispatchEventToDelegate(native_event);
}
void WaylandWindow::HandleSurfaceConfigure(int32_t widht,
int32_t height,
bool is_maximized,
bool is_fullscreen,
bool is_activated) {
NOTREACHED()
<< "Only shell surfaces must receive HandleSurfaceConfigure calls.";
}
void WaylandWindow::HandlePopupConfigure(const gfx::Rect& bounds_dip) {
NOTREACHED() << "Only shell popups must receive HandlePopupConfigure calls.";
}
void WaylandWindow::OnCloseRequest() {
delegate_->OnCloseRequest();
}
void WaylandWindow::OnDragEnter(const gfx::PointF& point,
std::unique_ptr<OSExchangeData> data,
int operation) {}
int WaylandWindow::OnDragMotion(const gfx::PointF& point, int operation) {
return -1;
}
void WaylandWindow::OnDragDrop() {}
void WaylandWindow::OnDragLeave() {}
void WaylandWindow::OnDragSessionClose(uint32_t dnd_action) {}
void WaylandWindow::SetBoundsDip(const gfx::Rect& bounds_dip) {
SetBounds(gfx::ScaleToRoundedRect(bounds_dip, buffer_scale()));
}
bool WaylandWindow::Initialize(PlatformWindowInitProperties properties) {
root_surface_ = std::make_unique<WaylandSurface>(connection_, this);
if (!root_surface_->Initialize()) {
LOG(ERROR) << "Failed to create wl_surface";
return false;
}
// Properties contain DIP bounds but the buffer scale is initially 1 so it's
// OK to assign. The bounds will be recalculated when the buffer scale
// changes.
bounds_px_ = properties.bounds;
opacity_ = properties.opacity;
type_ = properties.type;
connection_->wayland_window_manager()->AddWindow(GetWidget(), this);
if (!OnInitialize(std::move(properties)))
return false;
if (wayland_overlay_delegation_enabled_) {
primary_subsurface_ =
std::make_unique<WaylandSubsurface>(connection_, this);
if (!primary_subsurface_->surface())
return false;
connection_->wayland_window_manager()->AddSubsurface(
GetWidget(), primary_subsurface_.get());
}
connection_->ScheduleFlush();
PlatformEventSource::GetInstance()->AddPlatformEventDispatcher(this);
delegate_->OnAcceleratedWidgetAvailable(GetWidget());
// Will do nothing for menus because they have got their scale above.
UpdateBufferScale(false);
root_surface_->SetOpaqueRegion(gfx::Rect(bounds_px_.size()));
return true;
}
WaylandWindow* WaylandWindow::GetRootParentWindow() {
return parent_window_ ? parent_window_->GetRootParentWindow() : this;
}
void WaylandWindow::AddEnteredOutputId(struct wl_output* output) {
// Wayland does weird things for menus so instead of tracking outputs that
// we entered or left, we take that from the parent window and ignore this
// event.
if (wl::IsMenuType(type()))
return;
const uint32_t entered_output_id =
connection_->wayland_output_manager()->GetIdForOutput(output);
DCHECK_NE(entered_output_id, 0u);
auto result = entered_outputs_ids_.insert(entered_output_id);
DCHECK(result.first != entered_outputs_ids_.end());
UpdateBufferScale(true);
}
void WaylandWindow::RemoveEnteredOutputId(struct wl_output* output) {
// Wayland does weird things for menus so instead of tracking outputs that
// we entered or left, we take that from the parent window and ignore this
// event.
if (wl::IsMenuType(type()))
return;
const uint32_t left_output_id =
connection_->wayland_output_manager()->GetIdForOutput(output);
auto entered_output_id_it = entered_outputs_ids_.find(left_output_id);
// Workaround: when a user switches physical output between two displays,
// a window does not necessarily receive enter events immediately or until
// a user resizes/moves the window. It means that switching output between
// displays in a single output mode results in leave events, but the surface
// might not have received enter event before. Thus, remove the id of left
// output only if it was stored before.
if (entered_output_id_it != entered_outputs_ids_.end())
entered_outputs_ids_.erase(entered_output_id_it);
UpdateBufferScale(true);
}
void WaylandWindow::UpdateCursorPositionFromEvent(
std::unique_ptr<Event> event) {
DCHECK(event->IsLocatedEvent());
// This is a tricky part. Initially, Wayland sends events to surfaces the
// events are targeted for. But, in order to fulfill Chromium's assumptions
// about event targets, some of the events are rerouted and their locations
// are converted. The event we got here is rerouted and it has had its
// location fixed.
//
// Basically, this method must translate coordinates of all events
// in regards to top-level windows' coordinates as it's always located at
// origin (0,0) from Chromium point of view (remember that Wayland doesn't
// provide global coordinates to its clients). And it's totally fine to use it
// as the target. Thus, the location of the |event| is always converted using
// the top-level window's bounds as the target excluding cases, when the
// mouse/touch is over a top-level window.
auto* toplevel_window = GetRootParentWindow();
if (toplevel_window != this) {
ConvertEventLocationToTargetWindowLocation(
toplevel_window->GetBounds().origin(), GetBounds().origin(),
event->AsLocatedEvent());
}
auto* cursor_position = connection_->wayland_cursor_position();
if (cursor_position) {
cursor_position->OnCursorPositionChanged(
event->AsLocatedEvent()->location());
}
}
WaylandWindow* WaylandWindow::GetTopLevelWindow() {
return parent_window_ ? parent_window_->GetTopLevelWindow() : this;
}
WaylandWindow* WaylandWindow::GetTopMostChildWindow() {
return child_window_ ? child_window_->GetTopMostChildWindow() : this;
}
bool WaylandWindow::IsOpaqueWindow() const {
return opacity_ == ui::PlatformWindowOpacity::kOpaqueWindow;
}
bool WaylandWindow::IsActive() const {
// Please read the comment where the IsActive method is declared.
return false;
}
uint32_t WaylandWindow::DispatchEventToDelegate(
const PlatformEvent& native_event) {
auto* event = static_cast<Event*>(native_event);
if (event->IsLocatedEvent())
UpdateCursorPositionFromEvent(Event::Clone(*event));
bool handled = DispatchEventFromNativeUiEvent(
native_event, base::BindOnce(&PlatformWindowDelegate::DispatchEvent,
base::Unretained(delegate_)));
return handled ? POST_DISPATCH_STOP_PROPAGATION : POST_DISPATCH_NONE;
}
std::unique_ptr<WaylandSurface> WaylandWindow::TakeWaylandSurface() {
DCHECK(shutting_down_);
DCHECK(root_surface_);
root_surface_->UnsetRootWindow();
return std::move(root_surface_);
}
bool WaylandWindow::RequestSubsurface() {
auto subsurface = std::make_unique<WaylandSubsurface>(connection_, this);
if (!subsurface->surface())
return false;
connection_->wayland_window_manager()->AddSubsurface(GetWidget(),
subsurface.get());
subsurface_stack_above_.push_back(subsurface.get());
auto result = wayland_subsurfaces_.emplace(std::move(subsurface));
DCHECK(result.second);
return true;
}
bool WaylandWindow::ArrangeSubsurfaceStack(size_t above, size_t below) {
while (wayland_subsurfaces_.size() < above + below) {
if (!RequestSubsurface())
return false;
}
DCHECK(subsurface_stack_below_.size() + subsurface_stack_above_.size() >=
above + below);
if (subsurface_stack_above_.size() < above) {
auto splice_start = subsurface_stack_below_.begin();
for (size_t i = 0; i < below; ++i)
++splice_start;
subsurface_stack_above_.splice(subsurface_stack_above_.end(),
subsurface_stack_below_, splice_start,
subsurface_stack_below_.end());
} else if (subsurface_stack_below_.size() < below) {
auto splice_start = subsurface_stack_above_.end();
for (size_t i = 0; i < below - subsurface_stack_below_.size(); ++i)
--splice_start;
subsurface_stack_below_.splice(subsurface_stack_below_.end(),
subsurface_stack_above_, splice_start,
subsurface_stack_above_.end());
}
DCHECK(subsurface_stack_below_.size() >= below);
DCHECK(subsurface_stack_above_.size() >= above);
return true;
}
bool WaylandWindow::CommitOverlays(
std::vector<ui::ozone::mojom::WaylandOverlayConfigPtr>& overlays) {
// |overlays| is sorted from bottom to top.
std::sort(overlays.begin(), overlays.end(), OverlayStackOrderCompare);
// Find the location where z_oder becomes non-negative.
ozone::mojom::WaylandOverlayConfigPtr value =
ozone::mojom::WaylandOverlayConfig::New();
auto split = std::lower_bound(overlays.begin(), overlays.end(), value,
OverlayStackOrderCompare);
CHECK(split == overlays.end() || (*split)->z_order >= 0);
size_t num_primary_planes =
(split != overlays.end() && (*split)->z_order == 0) ? 1 : 0;
size_t above = (overlays.end() - split) - num_primary_planes;
size_t below = split - overlays.begin();
if (overlays.front()->z_order == INT32_MIN)
--below;
// Re-arrange the list of subsurfaces to fit the |overlays|. Request extra
// subsurfaces if needed.
if (!ArrangeSubsurfaceStack(above, below))
return false;
{
// Iterate through |subsurface_stack_below_|, setup subsurfaces and place
// them in corresponding order. Commit wl_buffers once a subsurface is
// configured.
auto overlay_iter = split - 1;
for (auto iter = subsurface_stack_below_.begin();
iter != subsurface_stack_below_.end(); ++iter, --overlay_iter) {
if (overlays.front()->z_order == INT32_MIN
? overlay_iter >= ++overlays.begin()
: overlay_iter >= overlays.begin()) {
WaylandSurface* reference_above = nullptr;
if (overlay_iter == split - 1) {
// It's possible that |overlays| does not contain primary plane, we
// still want to place relative to the surface with z_order=0.
reference_above = primary_subsurface_->wayland_surface();
} else {
reference_above = (*std::next(iter))->wayland_surface();
}
(*iter)->ConfigureAndShowSurface(
(*overlay_iter)->transform, (*overlay_iter)->crop_rect,
(*overlay_iter)->bounds_rect, (*overlay_iter)->enable_blend,
nullptr, reference_above);
connection_->buffer_manager_host()->CommitBufferInternal(
(*iter)->wayland_surface(), (*overlay_iter)->buffer_id, gfx::Rect(),
/*wait_for_frame_callback=*/false,
std::move((*overlay_iter)->access_fence_handle));
} else {
// If there're more subsurfaces requested that we don't need at the
// moment, hide them.
(*iter)->Hide();
}
}
// Iterate through |subsurface_stack_above_|, setup subsurfaces and place
// them in corresponding order. Commit wl_buffers once a subsurface is
// configured.
overlay_iter = split + num_primary_planes;
for (auto iter = subsurface_stack_above_.begin();
iter != subsurface_stack_above_.end(); ++iter, ++overlay_iter) {
if (overlay_iter < overlays.end()) {
WaylandSurface* reference_below = nullptr;
if (overlay_iter == split + num_primary_planes) {
// It's possible that |overlays| does not contain primary plane, we
// still want to place relative to the surface with z_order=0.
reference_below = primary_subsurface_->wayland_surface();
} else {
reference_below = (*std::prev(iter))->wayland_surface();
}
(*iter)->ConfigureAndShowSurface(
(*overlay_iter)->transform, (*overlay_iter)->crop_rect,
(*overlay_iter)->bounds_rect, (*overlay_iter)->enable_blend,
reference_below, nullptr);
connection_->buffer_manager_host()->CommitBufferInternal(
(*iter)->wayland_surface(), (*overlay_iter)->buffer_id, gfx::Rect(),
/*wait_for_frame_callback=*/false,
std::move((*overlay_iter)->access_fence_handle));
} else {
// If there're more subsurfaces requested that we don't need at the
// moment, hide them.
(*iter)->Hide();
}
}
}
if (!wayland_overlay_delegation_enabled_) {
connection_->buffer_manager_host()->CommitBufferInternal(
root_surface(), (*split)->buffer_id, (*split)->damage_region,
/*wait_for_frame_callback=*/true);
return true;
}
if (num_primary_planes) {
primary_subsurface_->ConfigureAndShowSurface(
(*split)->transform, (*split)->crop_rect, (*split)->bounds_rect,
(*split)->enable_blend, nullptr, nullptr);
connection_->buffer_manager_host()->CommitBufferInternal(
primary_subsurface_->wayland_surface(), (*split)->buffer_id,
(*split)->damage_region,
/*wait_for_frame_callback=*/false,
std::move((*split)->access_fence_handle));
}
root_surface_->SetViewportDestination(bounds_px_.size());
if (overlays.front()->z_order == INT32_MIN) {
background_buffer_id_ = overlays.front()->buffer_id;
should_attach_background_buffer_ = true;
}
if (should_attach_background_buffer_) {
connection_->buffer_manager_host()->CommitBufferInternal(
root_surface(), background_buffer_id_, /*damage_region=*/gfx::Rect(),
/*wait_for_frame_callback=*/true);
should_attach_background_buffer_ = false;
} else {
// Subsurfaces are set to sync, above surface configs will only take effect
// when root_surface is committed.
connection_->buffer_manager_host()->CommitWithoutBufferInternal(
root_surface(), /*wait_for_frame_callback=*/true);
}
return true;
}
} // namespace ui