Merge pull request #1361 from jason-simmons/edit_text_scrollable_fn3
Allow the Input/EditableText widget to scroll horizontally
This commit is contained in:
commit
02c2e79b64
@ -9,6 +9,7 @@ export 'package:sky/src/rendering/auto_layout.dart';
|
|||||||
export 'package:sky/src/rendering/block.dart';
|
export 'package:sky/src/rendering/block.dart';
|
||||||
export 'package:sky/src/rendering/box.dart';
|
export 'package:sky/src/rendering/box.dart';
|
||||||
export 'package:sky/src/rendering/debug.dart';
|
export 'package:sky/src/rendering/debug.dart';
|
||||||
|
export 'package:sky/src/rendering/editable_paragraph.dart';
|
||||||
export 'package:sky/src/rendering/error.dart';
|
export 'package:sky/src/rendering/error.dart';
|
||||||
export 'package:sky/src/rendering/flex.dart';
|
export 'package:sky/src/rendering/flex.dart';
|
||||||
export 'package:sky/src/rendering/grid.dart';
|
export 'package:sky/src/rendering/grid.dart';
|
||||||
|
@ -7,13 +7,11 @@ import 'dart:sky' as sky;
|
|||||||
|
|
||||||
import 'package:mojo_services/keyboard/keyboard.mojom.dart';
|
import 'package:mojo_services/keyboard/keyboard.mojom.dart';
|
||||||
import 'package:sky/painting.dart';
|
import 'package:sky/painting.dart';
|
||||||
|
import 'package:sky/rendering.dart';
|
||||||
import 'package:sky/src/fn3/basic.dart';
|
import 'package:sky/src/fn3/basic.dart';
|
||||||
import 'package:sky/src/fn3/framework.dart';
|
import 'package:sky/src/fn3/framework.dart';
|
||||||
|
|
||||||
const _kCursorBlinkPeriod = 500; // milliseconds
|
const _kCursorBlinkPeriod = 500; // milliseconds
|
||||||
const _kCursorGap = 1.0;
|
|
||||||
const _kCursorHeightOffset = 2.0;
|
|
||||||
const _kCursorWidth = 1.0;
|
|
||||||
|
|
||||||
typedef void StringUpdated();
|
typedef void StringUpdated();
|
||||||
|
|
||||||
@ -138,12 +136,17 @@ class EditableText extends StatefulComponent {
|
|||||||
this.value,
|
this.value,
|
||||||
this.focused: false,
|
this.focused: false,
|
||||||
this.style,
|
this.style,
|
||||||
this.cursorColor}) : super(key: key);
|
this.cursorColor,
|
||||||
|
this.onContentSizeChanged,
|
||||||
|
this.scrollOffset
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final EditableString value;
|
final EditableString value;
|
||||||
final bool focused;
|
final bool focused;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
final Color cursorColor;
|
final Color cursorColor;
|
||||||
|
final SizeChangedCallback onContentSizeChanged;
|
||||||
|
final Offset scrollOffset;
|
||||||
|
|
||||||
EditableTextState createState() => new EditableTextState();
|
EditableTextState createState() => new EditableTextState();
|
||||||
}
|
}
|
||||||
@ -183,20 +186,6 @@ class EditableTextState extends State<EditableText> {
|
|||||||
_showCursor = false;
|
_showCursor = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _paintCursor(sky.Canvas canvas, Size size) {
|
|
||||||
if (!_showCursor)
|
|
||||||
return;
|
|
||||||
|
|
||||||
double cursorHeight = config.style.fontSize + 2.0 * _kCursorHeightOffset;
|
|
||||||
Rect cursorRect = new Rect.fromLTWH(
|
|
||||||
_kCursorGap,
|
|
||||||
(size.height - cursorHeight) / 2.0,
|
|
||||||
_kCursorWidth,
|
|
||||||
cursorHeight
|
|
||||||
);
|
|
||||||
canvas.drawRect(cursorRect, new Paint()..color = config.cursorColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(config.style != null);
|
assert(config.style != null);
|
||||||
assert(config.focused != null);
|
assert(config.focused != null);
|
||||||
@ -207,29 +196,72 @@ class EditableTextState extends State<EditableText> {
|
|||||||
else if (!config.focused && _cursorTimer != null)
|
else if (!config.focused && _cursorTimer != null)
|
||||||
_stopCursorTimer();
|
_stopCursorTimer();
|
||||||
|
|
||||||
final EditableString value = config.value;
|
return new _EditableTextWidget(
|
||||||
final TextStyle style = config.style;
|
value: config.value,
|
||||||
|
style: config.style,
|
||||||
Widget text;
|
cursorColor: config.cursorColor,
|
||||||
if (value.composing.isValid) {
|
showCursor: _showCursor,
|
||||||
TextStyle composingStyle = style.merge(const TextStyle(decoration: underline));
|
onContentSizeChanged: config.onContentSizeChanged,
|
||||||
text = new StyledText(elements: [
|
scrollOffset: config.scrollOffset
|
||||||
style,
|
|
||||||
value.textBefore(value.composing),
|
|
||||||
[composingStyle, value.textInside(value.composing)],
|
|
||||||
value.textAfter(value.composing)
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
// TODO(eseidel): This is the wrong height if empty!
|
|
||||||
text = new Text(value.text, style: style);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget cursor = new Container(
|
|
||||||
height: style.fontSize * style.height,
|
|
||||||
width: _kCursorGap + _kCursorWidth,
|
|
||||||
child: new CustomPaint(callback: _paintCursor, token: _showCursor)
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return new Row([text, cursor]);
|
}
|
||||||
|
|
||||||
|
class _EditableTextWidget extends LeafRenderObjectWidget {
|
||||||
|
_EditableTextWidget({
|
||||||
|
Key key,
|
||||||
|
this.value,
|
||||||
|
this.style,
|
||||||
|
this.cursorColor,
|
||||||
|
this.showCursor,
|
||||||
|
this.onContentSizeChanged,
|
||||||
|
this.scrollOffset
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final EditableString value;
|
||||||
|
final TextStyle style;
|
||||||
|
final Color cursorColor;
|
||||||
|
final bool showCursor;
|
||||||
|
final SizeChangedCallback onContentSizeChanged;
|
||||||
|
final Offset scrollOffset;
|
||||||
|
|
||||||
|
RenderEditableParagraph createRenderObject() {
|
||||||
|
return new RenderEditableParagraph(
|
||||||
|
text: _buildTextSpan(),
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
showCursor: showCursor,
|
||||||
|
onContentSizeChanged: onContentSizeChanged,
|
||||||
|
scrollOffset: scrollOffset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateRenderObject(RenderEditableParagraph renderObject,
|
||||||
|
_EditableTextWidget oldWidget) {
|
||||||
|
renderObject.text = _buildTextSpan();
|
||||||
|
renderObject.cursorColor = cursorColor;
|
||||||
|
renderObject.showCursor = showCursor;
|
||||||
|
renderObject.onContentSizeChanged = onContentSizeChanged;
|
||||||
|
renderObject.scrollOffset = scrollOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a TextSpan that renders the EditableString using the chosen style.
|
||||||
|
TextSpan _buildTextSpan() {
|
||||||
|
if (value.composing.isValid) {
|
||||||
|
TextStyle composingStyle = style.merge(
|
||||||
|
const TextStyle(decoration: underline)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new StyledTextSpan(style, [
|
||||||
|
new PlainTextSpan(value.textBefore(value.composing)),
|
||||||
|
new StyledTextSpan(composingStyle, [
|
||||||
|
new PlainTextSpan(value.textInside(value.composing))
|
||||||
|
]),
|
||||||
|
new PlainTextSpan(value.textAfter(value.composing))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StyledTextSpan(style, [
|
||||||
|
new PlainTextSpan(value.text)
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:sky/animation.dart';
|
||||||
import 'package:sky/services.dart';
|
import 'package:sky/services.dart';
|
||||||
import 'package:sky/painting.dart';
|
import 'package:sky/painting.dart';
|
||||||
|
import 'package:sky/rendering.dart';
|
||||||
import 'package:sky/src/fn3/basic.dart';
|
import 'package:sky/src/fn3/basic.dart';
|
||||||
import 'package:sky/src/fn3/editable_text.dart';
|
import 'package:sky/src/fn3/editable_text.dart';
|
||||||
import 'package:sky/src/fn3/focus.dart';
|
import 'package:sky/src/fn3/focus.dart';
|
||||||
import 'package:sky/src/fn3/framework.dart';
|
import 'package:sky/src/fn3/framework.dart';
|
||||||
|
import 'package:sky/src/fn3/scrollable.dart';
|
||||||
import 'package:sky/src/fn3/theme.dart';
|
import 'package:sky/src/fn3/theme.dart';
|
||||||
|
|
||||||
export 'package:sky/services.dart' show KeyboardType;
|
export 'package:sky/services.dart' show KeyboardType;
|
||||||
@ -18,14 +21,18 @@ typedef void StringValueChanged(String value);
|
|||||||
// http://www.google.com/design/spec/components/text-fields.html#text-fields-single-line-text-field
|
// http://www.google.com/design/spec/components/text-fields.html#text-fields-single-line-text-field
|
||||||
const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0);
|
const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0);
|
||||||
|
|
||||||
class Input extends StatefulComponent {
|
class Input extends Scrollable {
|
||||||
Input({
|
Input({
|
||||||
GlobalKey key,
|
GlobalKey key,
|
||||||
this.initialValue: '',
|
this.initialValue: '',
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.keyboardType: KeyboardType.TEXT
|
this.keyboardType: KeyboardType.TEXT
|
||||||
}): super(key: key);
|
}): super(
|
||||||
|
key: key,
|
||||||
|
initialScrollOffset: 0.0,
|
||||||
|
scrollDirection: ScrollDirection.horizontal
|
||||||
|
);
|
||||||
|
|
||||||
final String initialValue;
|
final String initialValue;
|
||||||
final KeyboardType keyboardType;
|
final KeyboardType keyboardType;
|
||||||
@ -35,11 +42,14 @@ class Input extends StatefulComponent {
|
|||||||
InputState createState() => new InputState();
|
InputState createState() => new InputState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputState extends State<Input> {
|
class InputState extends ScrollableState<Input> {
|
||||||
String _value;
|
String _value;
|
||||||
EditableString _editableValue;
|
EditableString _editableValue;
|
||||||
KeyboardHandle _keyboardHandle = KeyboardHandle.unattached;
|
KeyboardHandle _keyboardHandle = KeyboardHandle.unattached;
|
||||||
|
|
||||||
|
double _contentWidth = 0.0;
|
||||||
|
double _containerWidth = 0.0;
|
||||||
|
|
||||||
void initState(BuildContext context) {
|
void initState(BuildContext context) {
|
||||||
super.initState(context);
|
super.initState(context);
|
||||||
_value = config.initialValue;
|
_value = config.initialValue;
|
||||||
@ -59,7 +69,7 @@ class InputState extends State<Input> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget build(BuildContext context) {
|
Widget buildContent(BuildContext context) {
|
||||||
ThemeData themeData = Theme.of(context);
|
ThemeData themeData = Theme.of(context);
|
||||||
bool focused = FocusState.at(context, config);
|
bool focused = FocusState.at(context, config);
|
||||||
|
|
||||||
@ -92,22 +102,25 @@ class InputState extends State<Input> {
|
|||||||
value: _editableValue,
|
value: _editableValue,
|
||||||
focused: focused,
|
focused: focused,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
cursorColor: cursorColor
|
cursorColor: cursorColor,
|
||||||
|
onContentSizeChanged: _handleContentSizeChanged,
|
||||||
|
scrollOffset: scrollOffsetVector
|
||||||
));
|
));
|
||||||
|
|
||||||
Border focusHighlight = new Border(bottom: new BorderSide(
|
|
||||||
color: focusHighlightColor,
|
|
||||||
width: focused ? 2.0 : 1.0
|
|
||||||
));
|
|
||||||
|
|
||||||
Container input = new Container(
|
|
||||||
child: new Stack(textChildren),
|
|
||||||
padding: _kTextfieldPadding,
|
|
||||||
decoration: new BoxDecoration(border: focusHighlight)
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Listener(
|
return new Listener(
|
||||||
child: input,
|
child: new SizeObserver(
|
||||||
|
callback: _handleContainerSizeChanged,
|
||||||
|
child: new Container(
|
||||||
|
child: new Stack(textChildren),
|
||||||
|
padding: _kTextfieldPadding,
|
||||||
|
decoration: new BoxDecoration(border: new Border(
|
||||||
|
bottom: new BorderSide(
|
||||||
|
color: focusHighlightColor,
|
||||||
|
width: focused ? 2.0 : 1.0
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
),
|
||||||
onPointerDown: (_) {
|
onPointerDown: (_) {
|
||||||
if (FocusState.at(context, config)) {
|
if (FocusState.at(context, config)) {
|
||||||
assert(_keyboardHandle.attached);
|
assert(_keyboardHandle.attached);
|
||||||
@ -125,4 +138,27 @@ class InputState extends State<Input> {
|
|||||||
_keyboardHandle.release();
|
_keyboardHandle.release();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScrollBehavior createScrollBehavior() => new BoundedBehavior();
|
||||||
|
BoundedBehavior get scrollBehavior => super.scrollBehavior;
|
||||||
|
|
||||||
|
void _handleContainerSizeChanged(Size newSize) {
|
||||||
|
_containerWidth = newSize.width;
|
||||||
|
_updateScrollBehavior();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleContentSizeChanged(Size newSize) {
|
||||||
|
_contentWidth = newSize.width;
|
||||||
|
_updateScrollBehavior();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateScrollBehavior() {
|
||||||
|
// Set the scroll offset to match the content width so that the cursor
|
||||||
|
// (which is always at the end of the text) will be visible.
|
||||||
|
scrollTo(scrollBehavior.updateExtents(
|
||||||
|
contentExtent: _contentWidth,
|
||||||
|
containerExtent: _containerWidth,
|
||||||
|
scrollOffset: _contentWidth)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -157,22 +157,33 @@ class TextPainter {
|
|||||||
_layoutRoot.maxHeight = value;
|
_layoutRoot.maxHeight = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
/// The width at which decreasing the width of the text would prevent it from painting itself completely within its bounds
|
/// The width at which decreasing the width of the text would prevent it from painting itself completely within its bounds
|
||||||
double get minContentWidth {
|
double get minContentWidth {
|
||||||
assert(!_needsLayout);
|
assert(!_needsLayout);
|
||||||
return _layoutRoot.rootElement.minContentWidth;
|
return _applyFloatingPointHack(_layoutRoot.rootElement.minContentWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The width at which increasing the width of the text no longer decreases the height
|
/// The width at which increasing the width of the text no longer decreases the height
|
||||||
double get maxContentWidth {
|
double get maxContentWidth {
|
||||||
assert(!_needsLayout);
|
assert(!_needsLayout);
|
||||||
return _layoutRoot.rootElement.maxContentWidth;
|
return _applyFloatingPointHack(_layoutRoot.rootElement.maxContentWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The height required to paint the text completely within its bounds
|
/// The height required to paint the text completely within its bounds
|
||||||
double get height {
|
double get height {
|
||||||
assert(!_needsLayout);
|
assert(!_needsLayout);
|
||||||
return _layoutRoot.rootElement.height;
|
return _applyFloatingPointHack(_layoutRoot.rootElement.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The distance from the top of the text to the first baseline of the given type
|
/// The distance from the top of the text to the first baseline of the given type
|
||||||
|
126
packages/flutter/lib/src/rendering/editable_paragraph.dart
Normal file
126
packages/flutter/lib/src/rendering/editable_paragraph.dart
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// 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 'package:sky/painting.dart';
|
||||||
|
import 'package:sky/src/rendering/box.dart';
|
||||||
|
import 'package:sky/src/rendering/object.dart';
|
||||||
|
import 'package:sky/src/rendering/paragraph.dart';
|
||||||
|
import 'package:sky/src/rendering/proxy_box.dart' show SizeChangedCallback;
|
||||||
|
|
||||||
|
const _kCursorGap = 1.0; // pixels
|
||||||
|
const _kCursorHeightOffset = 2.0; // pixels
|
||||||
|
const _kCursorWidth = 1.0; // pixels
|
||||||
|
|
||||||
|
/// A render object used by EditableText widgets. This is similar to
|
||||||
|
/// RenderParagraph but also renders a cursor and provides support for
|
||||||
|
/// scrolling.
|
||||||
|
class RenderEditableParagraph extends RenderParagraph {
|
||||||
|
|
||||||
|
RenderEditableParagraph({
|
||||||
|
TextSpan text,
|
||||||
|
Color cursorColor,
|
||||||
|
bool showCursor,
|
||||||
|
this.onContentSizeChanged,
|
||||||
|
Offset scrollOffset
|
||||||
|
}) : _cursorColor = cursorColor,
|
||||||
|
_showCursor = showCursor,
|
||||||
|
_scrollOffset = scrollOffset,
|
||||||
|
super(text);
|
||||||
|
|
||||||
|
Color _cursorColor;
|
||||||
|
bool _showCursor;
|
||||||
|
SizeChangedCallback onContentSizeChanged;
|
||||||
|
Offset _scrollOffset;
|
||||||
|
|
||||||
|
Size _contentSize;
|
||||||
|
|
||||||
|
Color get cursorColor => _cursorColor;
|
||||||
|
void set cursorColor(Color value) {
|
||||||
|
if (_cursorColor == value)
|
||||||
|
return;
|
||||||
|
_cursorColor = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get showCursor => _showCursor;
|
||||||
|
void set showCursor(bool value) {
|
||||||
|
if (_showCursor == value)
|
||||||
|
return;
|
||||||
|
_showCursor = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset get scrollOffset => _scrollOffset;
|
||||||
|
void set scrollOffset(Offset value) {
|
||||||
|
if (_scrollOffset == value)
|
||||||
|
return;
|
||||||
|
_scrollOffset = value;
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editable text does not support line wrap.
|
||||||
|
bool get allowLineWrap => false;
|
||||||
|
|
||||||
|
double _getIntrinsicWidth(BoxConstraints constraints) {
|
||||||
|
// There should be no difference between the minimum and maximum width
|
||||||
|
// because we only support single-line text.
|
||||||
|
layoutText(constraints);
|
||||||
|
return constraints.constrainWidth(
|
||||||
|
textPainter.maxContentWidth + _kCursorGap + _kCursorWidth
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
||||||
|
return _getIntrinsicWidth(constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
||||||
|
return _getIntrinsicWidth(constraints);
|
||||||
|
}
|
||||||
|
|
||||||
|
void performLayout() {
|
||||||
|
layoutText(constraints);
|
||||||
|
|
||||||
|
Size newContentSize = new Size(
|
||||||
|
textPainter.maxContentWidth + _kCursorGap + _kCursorWidth,
|
||||||
|
textPainter.height
|
||||||
|
);
|
||||||
|
size = constraints.constrain(newContentSize);
|
||||||
|
|
||||||
|
if (_contentSize == null || _contentSize != newContentSize) {
|
||||||
|
_contentSize = newContentSize;
|
||||||
|
if (onContentSizeChanged != null)
|
||||||
|
onContentSizeChanged(newContentSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
layoutText(constraints);
|
||||||
|
|
||||||
|
bool needsClipping = (_contentSize.width > size.width);
|
||||||
|
if (needsClipping) {
|
||||||
|
context.canvas.save();
|
||||||
|
context.canvas.clipRect(offset & size);
|
||||||
|
}
|
||||||
|
|
||||||
|
textPainter.paint(context.canvas, offset - _scrollOffset);
|
||||||
|
|
||||||
|
if (_showCursor) {
|
||||||
|
Rect cursorRect = new Rect.fromLTWH(
|
||||||
|
textPainter.maxContentWidth + _kCursorGap,
|
||||||
|
_kCursorHeightOffset,
|
||||||
|
_kCursorWidth,
|
||||||
|
size.height - 2.0 * _kCursorHeightOffset
|
||||||
|
);
|
||||||
|
context.canvas.drawRect(
|
||||||
|
cursorRect.shift(offset - _scrollOffset),
|
||||||
|
new Paint()..color = _cursorColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsClipping)
|
||||||
|
context.canvas.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,66 +8,57 @@ import 'package:sky/src/rendering/object.dart';
|
|||||||
|
|
||||||
export 'package:sky/src/painting/text_painter.dart';
|
export 'package:sky/src/painting/text_painter.dart';
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A render object that displays a paragraph of text
|
/// A render object that displays a paragraph of text
|
||||||
class RenderParagraph extends RenderBox {
|
class RenderParagraph extends RenderBox {
|
||||||
|
|
||||||
RenderParagraph(TextSpan text) : _textPainter = new TextPainter(text) {
|
RenderParagraph(
|
||||||
|
TextSpan text
|
||||||
|
) : textPainter = new TextPainter(text) {
|
||||||
assert(text != null);
|
assert(text != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
TextPainter _textPainter;
|
final TextPainter textPainter;
|
||||||
|
|
||||||
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
|
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
|
||||||
|
|
||||||
/// The text to display
|
/// The text to display
|
||||||
TextSpan get text => _textPainter.text;
|
TextSpan get text => textPainter.text;
|
||||||
void set text(TextSpan value) {
|
void set text(TextSpan value) {
|
||||||
if (_textPainter.text == value)
|
if (textPainter.text == value)
|
||||||
return;
|
return;
|
||||||
_textPainter.text = value;
|
textPainter.text = value;
|
||||||
_constraintsForCurrentLayout = null;
|
_constraintsForCurrentLayout = null;
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _layout(BoxConstraints constraints) {
|
// Whether the text should be allowed to wrap to multiple lines.
|
||||||
|
bool get allowLineWrap => true;
|
||||||
|
|
||||||
|
void layoutText(BoxConstraints constraints) {
|
||||||
assert(constraints != null);
|
assert(constraints != null);
|
||||||
if (_constraintsForCurrentLayout == constraints)
|
if (_constraintsForCurrentLayout == constraints)
|
||||||
return; // already cached this layout
|
return; // already cached this layout
|
||||||
_textPainter.maxWidth = constraints.maxWidth;
|
textPainter.maxWidth = allowLineWrap ? constraints.maxWidth : double.INFINITY;
|
||||||
_textPainter.minWidth = constraints.minWidth;
|
textPainter.minWidth = constraints.minWidth;
|
||||||
_textPainter.minHeight = constraints.minHeight;
|
textPainter.minHeight = constraints.minHeight;
|
||||||
_textPainter.maxHeight = constraints.maxHeight;
|
textPainter.maxHeight = constraints.maxHeight;
|
||||||
_textPainter.layout();
|
textPainter.layout();
|
||||||
_constraintsForCurrentLayout = constraints;
|
_constraintsForCurrentLayout = constraints;
|
||||||
}
|
}
|
||||||
|
|
||||||
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
||||||
_layout(constraints);
|
layoutText(constraints);
|
||||||
return constraints.constrainWidth(
|
return constraints.constrainWidth(textPainter.minContentWidth);
|
||||||
_applyFloatingPointHack(_textPainter.minContentWidth));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
||||||
_layout(constraints);
|
layoutText(constraints);
|
||||||
return constraints.constrainWidth(
|
return constraints.constrainWidth(textPainter.maxContentWidth);
|
||||||
_applyFloatingPointHack(_textPainter.maxContentWidth));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double _getIntrinsicHeight(BoxConstraints constraints) {
|
double _getIntrinsicHeight(BoxConstraints constraints) {
|
||||||
_layout(constraints);
|
layoutText(constraints);
|
||||||
return constraints.constrainHeight(
|
return constraints.constrainHeight(textPainter.height);
|
||||||
_applyFloatingPointHack(_textPainter.height));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
double getMinIntrinsicHeight(BoxConstraints constraints) {
|
double getMinIntrinsicHeight(BoxConstraints constraints) {
|
||||||
@ -80,15 +71,18 @@ class RenderParagraph extends RenderBox {
|
|||||||
|
|
||||||
double computeDistanceToActualBaseline(TextBaseline baseline) {
|
double computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||||
assert(!needsLayout);
|
assert(!needsLayout);
|
||||||
_layout(constraints);
|
layoutText(constraints);
|
||||||
return _textPainter.computeDistanceToActualBaseline(baseline);
|
return textPainter.computeDistanceToActualBaseline(baseline);
|
||||||
}
|
}
|
||||||
|
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
_layout(constraints);
|
layoutText(constraints);
|
||||||
// _paragraphPainter.width always expands to fill, use maxContentWidth instead.
|
|
||||||
size = constraints.constrain(new Size(_applyFloatingPointHack(_textPainter.maxContentWidth),
|
// We use textPainter.maxContentWidth here, rather that textPainter.width,
|
||||||
_applyFloatingPointHack(_textPainter.height)));
|
// because the latter is the width that it used to wrap the text, whereas
|
||||||
|
// the former is the actual width of the text.
|
||||||
|
size = constraints.constrain(new Size(textPainter.maxContentWidth,
|
||||||
|
textPainter.height));
|
||||||
}
|
}
|
||||||
|
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
@ -99,8 +93,8 @@ class RenderParagraph extends RenderBox {
|
|||||||
//
|
//
|
||||||
// TODO(abarth): Make computing the min/max intrinsic width/height
|
// TODO(abarth): Make computing the min/max intrinsic width/height
|
||||||
// a non-destructive operation.
|
// a non-destructive operation.
|
||||||
_layout(constraints);
|
layoutText(constraints);
|
||||||
_textPainter.paint(context.canvas, offset);
|
textPainter.paint(context.canvas, offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we should probably expose a way to do precise (inter-glpyh) hit testing
|
// we should probably expose a way to do precise (inter-glpyh) hit testing
|
||||||
|
Loading…
x
Reference in New Issue
Block a user