blob: aae42b3c3b22c440528de7808b65e89bb364e6cd [file] [log] [blame]
// Copyright 2018 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:developer' show Timeline;
import 'package:fidl/fidl.dart';
import 'package:fidl_fuchsia_modular/fidl.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
import 'package:fidl_fuchsia_ui_viewsv1token/fidl.dart';
import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
show ChildViewConnection;
import 'package:lib.app.dart/logging.dart';
import 'package:lib.widgets/model.dart';
import 'package:zircon/zircon.dart' show EventPair;
import '../tree/spanning_tree.dart';
import '../tree/tree.dart';
import 'surface.dart';
import 'surface_properties.dart';
// Data structure to manage the relationships and relative focus of surfaces
class SurfaceGraph extends Model {
SurfaceGraph() {
setupLogger(name: 'Mondrian');
}
SurfaceGraph.fromJson(Map<String, dynamic> json) {
reload(json);
}
/// Cache of surfaces
final Map<String, Surface> _surfaces = <String, Surface>{};
/// Surface relationship tree
final Tree<String> _tree = new Tree<String>(value: null);
/// The stack of previous focusedSurfaces, most focused at end
final List<String> _focusedSurfaces = <String>[];
/// The stack of previous focusedSurfaces, most focused at end
final Set<String> _dismissedSurfaces = <String>{};
/// A mapping between surfaces that were brought in as ModuleSource::External
/// surfaces (e.g. suggestions) and surfaces that were visually present at
/// their introduction, in order to track where to provide a shell affordance
/// for resummoning external surfaces that have been dismissed
/// (surfaces are identified by ID)
final Map<String, String> _visualAssociation = <String, String>{};
Tree<String> get root => _tree;
/// The node corresponding to the given id.
Surface getNode(String id) => _surfaces[id];
/// The last focused surface.
Surface _lastFocusedSurface;
/// The currently most focused [Surface]
Surface get focused =>
_focusedSurfaces.isEmpty ? null : _surfaces[_focusedSurfaces.last];
/// The history of focused [Surface]s
Iterable<Surface> get focusStack => _focusedSurfaces
.where(_surfaces.containsKey)
.map((String id) => _surfaces[id]);
/// Add a [Surface] to the graph with the given parameters.
///
/// Returns the surface that was added to the graph.
Surface addSurface(
String id,
SurfaceProperties properties,
String parentId,
SurfaceRelation relation,
String pattern,
String placeholderColor,
) {
Tree<String> node = _tree.find(id) ?? new Tree<String>(value: id);
Tree<String> parent =
(parentId == kNoParent) ? _tree : _tree.find(parentId);
assert(parent != null);
assert(relation != null);
Surface oldSurface = _surfaces[id];
Surface updatedSurface = new Surface(
this, node, properties, relation, pattern, placeholderColor);
// if this is an external surface, create an association between this and
// the most focused surface.
if (properties.source == ModuleSource.external$ &&
_focusedSurfaces.isNotEmpty) {
_visualAssociation[_focusedSurfaces.last] = id;
}
if (oldSurface != null) {
// TODO (jphsiao): this is a hack to handle the adding of a surface with
// the same view id. In this case we assume the view is going to be
// reused.
updatedSurface.connection = oldSurface.connection;
}
_surfaces[id] = updatedSurface;
// Do not add the child again if the parent already knows about it.
if (!parent.children.contains(node)) {
parent.add(node);
}
notifyListeners();
return updatedSurface;
}
/// Removes [Surface] from graph
void removeSurface(String id) {
if (_surfaces.keys.contains(id)) {
Tree<String> node = _tree.find(id);
if (node != null) {
node.detach();
// Remove orphaned children
for (Tree<String> child in node.children) {
child.detach();
// As a temporary policy, remove child surfaces when surfaces are
// removed. This policy will be revisited when we have a better sense
// of what to do with orphaned children.
_surfaces[child.value].remove();
_focusedSurfaces.remove(child.value);
_dismissedSurfaces.remove(child.value);
}
_focusedSurfaces.remove(id);
_dismissedSurfaces.remove(id);
_surfaces.remove(id);
notifyListeners();
}
}
}
/// Move the surface up in the focus stack, undismissing it if needed.
void focusSurface(String id) {
if (!_surfaces.containsKey(id)) {
log.warning('Invalid surface id "$id"');
return;
}
_dismissedSurfaces.remove(id);
_focusedSurfaces
..remove(id)
..add(id);
// Also request the input focus through the child view connection.
ChildViewConnection connection = _surfaces[id].connection;
if (connection != null) {
_surfaces[id].connection.requestFocus();
_lastFocusedSurface = _surfaces[id];
notifyListeners();
}
}
/// Add a container root to the surface graph
void addContainer(
String id,
SurfaceProperties properties,
String parentId,
SurfaceRelation relation,
List<ContainerLayout> layouts,
) {
// TODO (djurphy): collisions/pathing - partial fix if we
// make the changes so container IDs are paths.
log.info('addContainer: $id');
Tree<String> node = _tree.find(id) ?? new Tree<String>(value: id);
log.info('found or made node: $node');
Tree<String> parent =
(parentId == kNoParent) ? _tree : _tree.find(parentId);
assert(parent != null);
assert(relation != null);
parent.add(node);
Surface oldSurface = _surfaces[id];
_surfaces[id] = new SurfaceContainer(
this, node, properties, relation, '' /*pattern*/, layouts);
oldSurface?.notifyListeners();
log.info('_surfaces[id]: ${_surfaces[id]}');
notifyListeners();
}
/// Returns the list of surfaces that would be dismissed if this surface
/// were dismissed - e.g. as a result of dependency - including this surface
List<String> dismissedSet(String id) {
Surface dismissed = _surfaces[id];
List<Surface> ancestors = dismissed.ancestors.toList();
List<Surface> dependentTree = getDependentSpanningTree(dismissed)
.map((Tree<Surface> t) => t.value)
.toList()
// TODO(djmurphy) - when codependent comes in this needs to change
// this only removes down the tree, codependents would remove their
// ancestors
..removeWhere((Surface s) => ancestors.contains(s));
List<String> depIds =
dependentTree.map((Surface s) => s.node.value).toList();
return depIds;
}
/// Check if given surface can be dismissed
bool canDismissSurface(String id) {
List<String> wouldDismiss = dismissedSet(id);
return _focusedSurfaces
.where((String fid) => !wouldDismiss.contains(fid))
.isNotEmpty;
}
/// When called surface is no longer displayed
bool dismissSurface(String id) {
if (!canDismissSurface(id)) {
return false;
}
List<String> depIds = dismissedSet(id);
_focusedSurfaces.removeWhere((String fid) => depIds.contains(fid));
_dismissedSurfaces.addAll(depIds);
notifyListeners();
return true;
}
/// True if surface has been dismissed and not subsequently focused
bool isDismissed(String id) => _dismissedSurfaces.contains(id);
void connectView(String id, InterfaceHandle<ViewOwner> viewOwner) {
connectViewFromViewHolderToken(
id,
ViewHolderToken(
value: EventPair(viewOwner.passChannel().passHandle())));
}
/// Used to update a [Surface] with a live ChildViewConnection
void connectViewFromViewHolderToken(
String id, ViewHolderToken viewHolderToken) {
final Surface surface = _surfaces[id];
if (surface != null) {
if (surface.connection != null) {
// TODO(jphsiao): remove this hack once story shell API has been
// change to accomodate view reusage
return;
}
log.fine('connectView $surface');
surface
..connection = ChildViewConnection(
viewHolderToken,
onAvailable: (ChildViewConnection connection) {
Timeline.instantSync('surface available', arguments: {'id': '$id'});
// If this surface is the last focused one, also request input focus
if (_lastFocusedSurface == surface) {
connection.requestFocus();
}
surface.notifyListeners();
},
onUnavailable: (ChildViewConnection connection) {
trace('surface $id unavailable');
surface.connection = null;
if (_surfaces.containsValue(surface)) {
removeSurface(id);
notifyListeners();
}
// Also any existing listener
surface.notifyListeners();
},
)
..notifyListeners();
}
}
void reload(Map<String, dynamic> json) {
List<dynamic> decodedSurfaceList = json['surfaceList'];
for (dynamic s in decodedSurfaceList) {
Map<String, dynamic> item = s.cast<String, dynamic>();
Surface surface = new Surface.fromJson(item, this);
_surfaces.putIfAbsent(surface.node.value, () {
return surface;
});
}
_surfaces.forEach((String id, Surface surface) {
Tree<String> node = surface.node;
if (surface.isParentRoot) {
_tree.add(node);
}
if (surface.childIds != null) {
for (String id in surface.childIds) {
node.add(_surfaces[id].node);
}
}
});
dynamic list = json['focusStack'];
List<String> focusStack = list.cast<String>();
_focusedSurfaces.addAll(focusStack);
}
// Get the SurfaceIds of associated external surfaces
// (surfaces originating from outside the current story)
// that are dismissed and associated with the current Surface
Set<String> externalSurfaces({String surfaceId}) {
// Case1: An external child has a relationship with this surface
// and the child has been dismissed
Surface parent = getNode(surfaceId);
List<Surface> externalSurfaces = parent.children.toList()
..retainWhere(
(Surface s) => s.properties.source == ModuleSource.external$);
Set<String> externalIds =
externalSurfaces.map((Surface s) => s.node.value).toSet();
// Case2: The focused surface has a recorded visual association with an
// external surface
if (_visualAssociation[surfaceId].isNotEmpty) {
externalIds.add(_visualAssociation[surfaceId]);
}
return externalIds;
}
/// Returns the amount of [Surface]s in the graph
int get size => _surfaces.length;
/// The tree size includes the root node which has no surface
int get treeSize => _tree.flatten().length;
@override
String toString() =>
'Tree:\n${_tree.children.map(_toString).join('\n')}\nfocusStack length ${focusStack.length}';
String _toString(Tree<String> node, {String prefix = ''}) {
String nodeString = '$prefix${_surfaces[node.value]}';
if (node.children.isNotEmpty) {
nodeString =
'$nodeString\n${node.children.map((Tree<String> node) => _toString(node, prefix: '$prefix ')).join('\n')}';
}
return '$nodeString';
}
Map<String, dynamic> toJson() {
return {
'surfaceList': _surfaces.values.toList(),
'focusStack': _focusedSurfaces,
'links': [], // TODO(jphsiao): plumb through link data
};
}
}