blob: d9c9efd2873e9a139dd3e0db2770573d908906af [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:ermine/src/services/presenter_service.dart';
import 'package:ermine/src/states/view_state.dart';
import 'package:ermine_utils/ermine_utils.dart';
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_scenic_flutter/fuchsia_view.dart';
import 'package:mobx/mobx.dart';
/// Defines an implementation of [ViewState].
class ViewStateImpl with Disposable implements ViewState {
@override
final FuchsiaViewConnection viewConnection;
@override
final String title;
@override
final String? id;
@override
final String? url;
@override
final ViewHandle view;
final Observable<bool> _rendered = false.asObservable();
final Observable<bool> _connected = false.asObservable();
@override
bool get timeout => _timeout.value;
final Observable<bool> _timeout = false.asObservable();
@override
bool get closed => _closed.value;
final Observable<bool> _closed = false.asObservable();
/// Returns true when the view is connected to the view tree but has not
/// rendered a frame.
@override
bool get loading => _connected.value;
/// Returns true when the view has rendered a frame.
@override
bool get loaded => _rendered.value;
@override
bool get visible {
return viewConnection.viewport?.isEmpty == false;
}
ViewStateImpl({
required this.viewConnection,
required this.view,
required this.title,
required VoidCallback onClose,
this.id,
this.url,
}) : _onClose = onClose;
@override
bool get hitTestable => _hitTestable.value;
@override
set hitTestable(bool value) => _hitTestable.value = value;
final Observable<bool> _hitTestable = true.asObservable();
@override
bool get focusable => _focusable.value;
@override
set focusable(bool value) => _focusable.value = value;
final Observable<bool> _focusable = true.asObservable();
@override
Rect? get viewport => viewConnection.viewport;
final VoidCallback _onClose;
@override
void close() => runInAction(() {
_closed.value = true;
_onClose();
});
@override
void wait() => runInAction(() {
_timeout.value = false;
Future.delayed(Duration(seconds: 10),
() => runInAction(() => _timeout.value = true));
});
/// Called by [PresenterService] when the view is attached to the scene.
void viewConnected() {
// Mark the view as ready, so that it can be focused.
runInAction(() => _connected.value = true);
// Mark the view as timed-out if it fails to render in time.
Future.delayed(
Duration(seconds: 10), () => runInAction(() => _timeout.value = true));
}
/// Called by [PresenterService] when the view has rendered a new frame.
void viewStateChanged({required bool state}) {
runInAction(() {
// Mark the view as ready, so that it can be focused.
_connected.value = _connected.value || state;
// The view has rendered its first frame.
_rendered.value = true;
// Cancel timeout waiting for the view to render its first frame.
_timeout.value = false;
});
}
bool _requestFocusPending = false;
static const _kMaxRetries = 10;
static const _kBackoffDuration = Duration(milliseconds: 8);
/// Recursively request focus on this view with [_kMaxRetries] and exponential
/// [backOff].
///
/// Use [FocusState] instance to request focus on the underlying scenic view,
/// retrying recursively.The retry is also aborted if [cancelSetFocus]
/// is issued while it was pending.
@override
void setFocus({
int retry = _kMaxRetries,
Duration backOff = _kBackoffDuration,
}) {
// Flatland does not send viewStateChanged events per frame, so setting
// _connected to false would be a one way transition and would prevent
// this view from ever becoming focusable again.
if (!viewConnection.useFlatland && (loaded && !visible)) {
// Reset connected flag to retry setFocus on next viewStateChanged.
_connected.value = false;
} else {
if (retry < _kMaxRetries && !_requestFocusPending) {
return;
}
_requestFocusPending = true;
viewConnection.requestFocus().then((_) {
_requestFocusPending = false;
}).catchError((e) {
if (retry > 0) {
Future.delayed(backOff, () {
setFocus(
retry: retry - 1,
backOff: Duration(milliseconds: backOff.inMilliseconds * 2),
);
});
} else {
log.warning('Failed to set focus on $title: $e');
}
});
}
}
@override
void cancelSetFocus() {
_requestFocusPending = false;
}
}