flutter_flutter/sdk/lib/rendering/paragraph.dart
Hixie d79ea98da2 Make popup menus line up to their baseline per the Material spec.
This entails:
 - Making the baseline logic cache results.
 - Making the baseline logic track who used its information.
 - Making the baseline logic mark all ancestors up to whoever used
   its information wheneven its node gets markNeedsLayout.
 - Making RenderShrinkWrapWidth make its child respect the step width
   and step height, rather than just sizing the child then snapping.
   This is required to make the ink splashes render right on menus
   that are snapped.
 - Adding debugDescribeSettings() to RenderShrinkWrapWidth.
 - Introducing a RenderBaseline class that offsets its child to a
   certain baseline.
 - Factoring out some common code from RenderBaseline and
   RenderPositionedBox to RenderShiftedBox.
 - Redoing all the menu layout logic.

BUG=
R=abarth@chromium.org

Review URL: https://codereview.chromium.org/1219113003.
2015-07-01 12:24:45 -07:00

199 lines
6.2 KiB
Dart

// Copyright 2015 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.
import 'dart:sky' as sky;
import 'box.dart';
import 'object.dart';
import '../painting/text_style.dart';
abstract class InlineBase {
sky.Node _toDOM(sky.Document owner);
String toString([String prefix = '']);
}
class InlineText extends InlineBase {
InlineText(this.text) {
assert(text != null);
}
final String text;
sky.Node _toDOM(sky.Document owner) {
return owner.createText(text);
}
bool operator ==(other) => other is InlineText && text == other.text;
int get hashCode => text.hashCode;
String toString([String prefix = '']) => '${prefix}InlineText: "${text}"';
}
class InlineStyle extends InlineBase {
InlineStyle(this.style, this.children) {
assert(style != null);
assert(children != null);
}
final TextStyle style;
final List<InlineBase> children;
sky.Node _toDOM(sky.Document owner) {
sky.Element parent = owner.createElement('t');
style.applyToCSSStyle(parent.style);
for (InlineBase child in children) {
parent.appendChild(child._toDOM(owner));
}
return parent;
}
bool operator ==(other) {
if (identical(this, other))
return true;
if (other is! InlineStyle
|| style != other.style
|| children.length != other.children.length)
return false;
for (int i = 0; i < children.length; ++i) {
if (children[i] != other.children[i])
return false;
}
return true;
}
int get hashCode {
int value = 373;
value = 37 * value + style.hashCode;
for (InlineBase child in children)
value = 37 * value + child.hashCode;
return value;
}
String toString([String prefix = '']) {
List<String> result = [];
result.add('${prefix}InlineStyle:');
var indent = '${prefix} ';
result.add('${style.toString(indent)}');
for (InlineBase child in children) {
result.add(child.toString(indent));
}
return result.join('\n');
}
}
// Unfortunately, using full precision floating point here causes bad layouts
// because floating point math isn't associative. If we add and subtract
// padding, for example, we'll get different values when we estimate sizes and
// when we actually compute layout because the operations will end up associated
// differently. To work around this problem for now, we round fractional pixel
// values up to the nearest whole pixel value. The right long-term fix is to do
// layout using fixed precision arithmetic.
double _applyFloatingPointHack(double layoutValue) {
return layoutValue.ceilToDouble();
}
class RenderParagraph extends RenderBox {
RenderParagraph(InlineBase inlineValue) {
_layoutRoot.rootElement = _document.createElement('p');
inline = inlineValue;
}
final sky.Document _document = new sky.Document();
final sky.LayoutRoot _layoutRoot = new sky.LayoutRoot();
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
InlineBase _inline;
InlineBase get inline => _inline;
void set inline (InlineBase value) {
if (_inline == value)
return;
_inline = value;
_layoutRoot.rootElement.setChild(_inline._toDOM(_document));
_constraintsForCurrentLayout = null;
markNeedsLayout();
}
void _layout(BoxConstraints constraints) {
assert(constraints != null);
if (_constraintsForCurrentLayout == constraints)
return; // already cached this layout
_layoutRoot.maxWidth = constraints.maxWidth;
_layoutRoot.minWidth = constraints.minWidth;
_layoutRoot.minHeight = constraints.minHeight;
_layoutRoot.maxHeight = constraints.maxHeight;
_layoutRoot.layout();
_constraintsForCurrentLayout = constraints;
}
double getMinIntrinsicWidth(BoxConstraints constraints) {
_layout(constraints);
return constraints.constrainWidth(
_applyFloatingPointHack(_layoutRoot.rootElement.minContentWidth));
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
_layout(constraints);
return constraints.constrainWidth(
_applyFloatingPointHack(_layoutRoot.rootElement.maxContentWidth));
}
double _getIntrinsicHeight(BoxConstraints constraints) {
_layout(constraints);
return constraints.constrainHeight(
_applyFloatingPointHack(_layoutRoot.rootElement.height));
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
return _getIntrinsicHeight(constraints);
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
return _getIntrinsicHeight(constraints);
}
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!needsLayout);
_layout(constraints);
sky.Element root = _layoutRoot.rootElement;
switch (baseline) {
case TextBaseline.alphabetic: return root.alphabeticBaseline;
case TextBaseline.ideographic: return root.ideographicBaseline;
}
}
void performLayout() {
_layout(constraints);
sky.Element root = _layoutRoot.rootElement;
// rootElement.width always expands to fill, use maxContentWidth instead.
size = constraints.constrain(new Size(_applyFloatingPointHack(root.maxContentWidth),
_applyFloatingPointHack(root.height)));
}
void paint(PaintingCanvas canvas, Offset offset) {
// Ideally we could compute the min/max intrinsic width/height with a
// non-destructive operation. However, currently, computing these values
// will destroy state inside the layout root. If that happens, we need to
// get back the correct state by calling _layout again.
//
// TODO(abarth): Make computing the min/max intrinsic width/height
// a non-destructive operation.
// TODO(ianh): Make LayoutRoot support a paint offset so we don't
// need to translate for each span of text.
_layout(constraints);
canvas.translate(offset.dx, offset.dy);
_layoutRoot.paint(canvas);
canvas.translate(-offset.dx, -offset.dy);
}
// we should probably expose a way to do precise (inter-glpyh) hit testing
String debugDescribeSettings(String prefix) {
String result = '${super.debugDescribeSettings(prefix)}';
result += '${prefix}inline:\n${inline.toString("$prefix ")}\n';
return result;
}
}