blob: aa0c3e9b725f7d41e411520c0a65719177ddf5a5 [file] [log] [blame]
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
part of layout;
/** The interface that the layout algorithms use to talk to the view. */
abstract class Positionable {
ViewLayout get layout;
/** Gets our custom CSS properties, as provided by the CSS preprocessor. */
Map<String, String> get customStyle;
/** Gets the root DOM used for layout. */
Element get node;
/** Gets the collection of child views. */
Iterable<Positionable> get childViews;
/** Causes a view to layout its children. */
void doLayout();
}
/**
* Caches the layout parameters that were specified in CSS during a layout
* computation. These values are immutable during a layout.
*/
class LayoutParams {
// TODO(jmesserly): should be const, but there's a bug in DartC preventing us
// from calling "window." in an initializer. See b/5332777
CssStyleDeclaration style;
int get layer => 0;
LayoutParams(Element node) {
style = node.getComputedStyle();
}
}
// TODO(jmesserly): enums would really help here
class Dimension {
// TODO(jmesserly): perhaps this should be X and Y
static const WIDTH = const Dimension._internal('width');
static const HEIGHT = const Dimension._internal('height');
final String name; // for debugging
const Dimension._internal(this.name);
}
class ContentSizeMode {
/** Minimum content size, e.g. min-width and min-height in CSS. */
static const MIN = const ContentSizeMode._internal('min');
/** Maximum content size, e.g. min-width and min-height in CSS. */
static const MAX = const ContentSizeMode._internal('max');
// TODO(jmesserly): we probably want some sort of "auto" or "best fit" mode
// Don't need it yet though.
final String name; // for debugging
const ContentSizeMode._internal(this.name);
}
/**
* Abstract base class for View layout. Tracks relevant layout state.
* This code was inspired by code in Android's View.java; it's needed for the
* rest of the layout system.
*/
class ViewLayout {
/**
* The layout parameters associated with this view and used by the parent
* to determine how this view should be laid out.
*/
LayoutParams layoutParams;
int _offsetWidth;
int _offsetHeight;
/** The view that this layout belongs to. */
final Positionable view;
/**
* To get a perforant positioning model on top of the DOM, we read all
* properties in the first pass while computing positions. Then we have a
* second pass that actually moves everything.
*/
int _measuredLeft, _measuredTop, _measuredWidth, _measuredHeight;
ViewLayout(this.view);
/**
* Creates the appropriate view layout, depending on the properties.
*/
// TODO(jmesserly): we should support user defined layouts somehow. Perhaps
// registered with a LayoutProvider.
factory ViewLayout.fromView(Positionable view) {
if (hasCustomLayout(view)) {
return new GridLayout(view);
} else {
return new ViewLayout(view);
}
}
static bool hasCustomLayout(Positionable view) {
return view.customStyle['display'] == "-dart-grid";
}
CssStyleDeclaration get _style => layoutParams.style;
void cacheExistingBrowserLayout() {
_offsetWidth = view.node.offset.width;
_offsetHeight = view.node.offset.height;
}
int get currentWidth {
return _offsetWidth;
}
int get currentHeight {
return _offsetHeight;
}
int get borderLeftWidth => _toPixels(_style.borderLeftWidth);
int get borderTopWidth => _toPixels(_style.borderTopWidth);
int get borderRightWidth => _toPixels(_style.borderRightWidth);
int get borderBottomWidth => _toPixels(_style.borderBottomWidth);
int get borderWidth => borderLeftWidth + borderRightWidth;
int get borderHeight => borderTopWidth + borderBottomWidth;
/** Implements the custom layout computation. */
void measureLayout(Future<Size> size, Completer<bool> changed) {}
/**
* Positions the view within its parent container.
* Also performs a layout of its children.
*/
void setBounds(int left, int top, int width, int height) {
assert(width >= 0 && height >= 0);
_measuredLeft = left;
_measuredTop = top;
// Note: we need to save the client height
_measuredWidth = width - borderWidth;
_measuredHeight = height - borderHeight;
final completer = new Completer<Size>();
completer.complete(new Size(_measuredWidth, _measuredHeight));
measureLayout(completer.future, null);
}
/** Applies the layout to the node. */
void applyLayout() {
if (_measuredLeft != null) {
// TODO(jmesserly): benchmark the performance of this DOM interaction
final style = view.node.style;
style.position = 'absolute';
style.left = '${_measuredLeft}px';
style.top = '${_measuredTop}px';
style.width = '${_measuredWidth}px';
style.height = '${_measuredHeight}px';
style.zIndex = '${layoutParams.layer}';
_measuredLeft = null;
_measuredTop = null;
_measuredWidth = null;
_measuredHeight = null;
// Ensure we can handle our custom layout when it is a child of a
// DOM-positioned node. For example, say we have a View tree like this:
//
// ViewWithLayout <-- uses our layout engine
// childView1 <-- is positioned by our layout engine, but uses
// HTML layout internally
// childOfChild <-- uses our layout engine for its own children
if (!hasCustomLayout(view)) {
for (final child in view.childViews) {
child.doLayout();
}
}
}
}
int measureContent(ViewLayout parent, Dimension dimension,
[ContentSizeMode mode = null]) {
if (dimension == Dimension.WIDTH) {
return measureWidth(parent, mode);
} else if (dimension == Dimension.HEIGHT) {
return measureHeight(parent, mode);
}
}
int measureWidth(ViewLayout parent, ContentSizeMode mode) {
final style = layoutParams.style;
if (mode == ContentSizeMode.MIN) {
return _styleToPixels(style.minWidth, currentWidth, parent.currentWidth);
} else if (mode == ContentSizeMode.MAX) {
return _styleToPixels(style.maxWidth, currentWidth, parent.currentWidth);
}
}
int measureHeight(ViewLayout parent, ContentSizeMode mode) {
final style = layoutParams.style;
if (mode == ContentSizeMode.MIN) {
return _styleToPixels(
style.minHeight, currentHeight, parent.currentHeight);
} else if (mode == ContentSizeMode.MAX) {
return _styleToPixels(
style.maxHeight, currentHeight, parent.currentHeight);
}
}
static int _toPixels(String style) {
if (style.endsWith('px')) {
return int.parse(style.substring(0, style.length - 2));
} else {
// TODO(jmesserly): other size units
throw new UnsupportedError(
'Unknown min/max content size format: "$style"');
}
}
static int _styleToPixels(String style, num size, num parentSize) {
if (style == 'none') {
// For an unset max-content size, use the actual size
return size;
}
if (style.endsWith('%')) {
num percent = double.parse(style.substring(0, style.length - 1));
return ((percent / 100) * parentSize).toInt();
}
return _toPixels(style);
}
}