mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
Specs: Initial hack at extensible style/layout
Review URL: https://codereview.chromium.org/716013002
This commit is contained in:
parent
af585507a0
commit
f8340b77ef
74
examples/style/block-layout.sky
Normal file
74
examples/style/block-layout.sky
Normal file
|
@ -0,0 +1,74 @@
|
|||
SKY MODULE
|
||||
<import src="sky:core" as="sky"/>
|
||||
<!--
|
||||
! this module provides trivial vertical block layout
|
||||
! no margins, padding, borders, etc
|
||||
!-->
|
||||
<script>
|
||||
module.exports.BlockLayoutManager = class BlockLayoutManager extends sky.LayoutManager {
|
||||
function layout(width, height) {
|
||||
if (width == null)
|
||||
width = this.getIntrinsicWidth().value;
|
||||
let autoHeight = false;
|
||||
if (height == null) {
|
||||
height = 0;
|
||||
autoHeight = true;
|
||||
}
|
||||
this.assumeDimensions(width, height);
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
let y = 0;
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
if (child.needsLayout) {
|
||||
let dims = child.layoutManager.layout(width, null);
|
||||
this.setChildSize(child, dims.width, dims.height);
|
||||
}
|
||||
this.setChildPosition(child, 0, y);
|
||||
y += child.height;
|
||||
loop = children.next();
|
||||
}
|
||||
if (autoHeight)
|
||||
height = y;
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
function getIntrinsicWidth() {
|
||||
let width = this.node.getProperty('width');
|
||||
if (typeof height != 'number') {
|
||||
// e.g. width: auto
|
||||
width = 0;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
let childWidth = child.layoutManager.getIntrinsicWidth();
|
||||
if (width < childWidth.value)
|
||||
width = childWidth.value;
|
||||
loop = children.next();
|
||||
}
|
||||
}
|
||||
return super(width); // applies and provides our own min-width/max-width rules
|
||||
}
|
||||
function getIntrinsicHeight() {
|
||||
let height = this.node.getProperty('height');
|
||||
if (typeof height != 'number') {
|
||||
// e.g. height: auto
|
||||
height = 0;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
let childHeight = child.layoutManager.getIntrinsicHeight();
|
||||
if (height < childHeight.value)
|
||||
height = childHeight.value;
|
||||
loop = children.next();
|
||||
}
|
||||
}
|
||||
return super(height); // applies and provides our own min-width/max-width rules
|
||||
}
|
||||
}
|
||||
sky.registerLayoutManager('block', module.exports.BlockLayoutManager);
|
||||
</script>
|
146
examples/style/hex-layout.sky
Normal file
146
examples/style/hex-layout.sky
Normal file
|
@ -0,0 +1,146 @@
|
|||
#!mojo mojo:sky
|
||||
<import src="sky:core" as="sky"/>
|
||||
<script>
|
||||
class BeehiveLayoutManager extends sky.LayoutManager {
|
||||
function layout(width, height) {
|
||||
if (width == null)
|
||||
width = this.getIntrinsicWidth().value;
|
||||
let autoHeight = false;
|
||||
if (height == null) {
|
||||
height = 0;
|
||||
autoHeight = true;
|
||||
}
|
||||
this.assumeDimensions(width, height);
|
||||
let cellCount = this.node.getProperty('beehive-count');
|
||||
let cellDim = width / cellCount;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
if (child.needsLayout) {
|
||||
child.layoutManager.layout(cellDim, cellDim);
|
||||
// we ignore the size the child reported from layout(), and force it to the cell dimensions
|
||||
this.setChildSize(child, cellDim, cellDim);
|
||||
}
|
||||
this.setChildPosition(child, x * cellDim + (y % 2) * cellDim/2, y * 3/4 * cellDim);
|
||||
x += 1;
|
||||
if (x > cellCount) {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
loop = children.next();
|
||||
}
|
||||
if (height == 0)
|
||||
height = (1 + y * 3/4) * cellDim;
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
function getIntrinsicWidth() {
|
||||
// this is the logic that LayoutManager.getIntrinsicWidth() has by default
|
||||
// shown here because I wrote it before realising it should be the default
|
||||
let width = this.node.getProperty('width');
|
||||
if (typeof width != 'number')
|
||||
width = 0;
|
||||
let minWidth = this.node.getProperty('min-width');
|
||||
if (typeof width != 'number')
|
||||
minWidth = 0;
|
||||
let maxWidth = this.node.getProperty('max-width');
|
||||
if (typeof width != 'number')
|
||||
maxWidth = Infinity;
|
||||
if (maxWidth < minWidth)
|
||||
maxWidth = minWidth;
|
||||
if (width > maxWidth)
|
||||
width = maxWidth;
|
||||
if (width < minWidth)
|
||||
width = minWidth;
|
||||
return {
|
||||
minimum: minWidth,
|
||||
value: width,
|
||||
maximum: maxWidth,
|
||||
};
|
||||
}
|
||||
function getIntrinsicHeight() {
|
||||
let height = this.node.getProperty('height');
|
||||
if (typeof height != 'number') {
|
||||
// e.g. height: auto
|
||||
width = this.getIntrinsicWidth().value;
|
||||
let cellCount = this.node.getProperty('beehive-count');
|
||||
let cellDim = width / cellCount;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
let childCount = 0;
|
||||
while (!loop.done) {
|
||||
childCount += 1;
|
||||
loop.next();
|
||||
}
|
||||
if (childCount > 0)
|
||||
height = cellDim * (1/4 + Math.ceil(childCount / cellCount) * 3/4);
|
||||
else
|
||||
height = 0;
|
||||
}
|
||||
return super(height); // does the equivalent of getIntrinsicWidth() above, applying min-height etc
|
||||
}
|
||||
function paintChildren(RenderingSurface canvas) {
|
||||
let width = this.node.width;
|
||||
let cellCount = this.node.getProperty('beehive-count');
|
||||
let cellDim = width / cellCount;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
if (child.needsPaint) {
|
||||
canvas.save();
|
||||
try {
|
||||
canvas.beginPath();
|
||||
canvas.moveTo(child.x, child.y + cellDim/4);
|
||||
canvas.lineTo(child.x + cellDim/2, child.y);
|
||||
canvas.lineTo(child.x + cellDim, child.y + cellDim/4);
|
||||
canvas.lineTo(child.x + cellDim, child.y + 3*cellDim/4);
|
||||
canvas.lineTo(child.x + cellDim/2, child.y + cellDim);
|
||||
canvas.moveTo(child.x, child.y + 3*cellDim/4);
|
||||
canvas.closePath();
|
||||
canvas.clip();
|
||||
child.paint(canvas);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
loop = children.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
sky.registerLayoutManager('beehive', BeehiveLayoutManager);
|
||||
let BeehiveCountStyleValueType = new StyleValueType();
|
||||
BeehiveCountStyleValueType.addParser((tokens) => {
|
||||
let token = tokens.next();
|
||||
if (token.done) throw new Error();
|
||||
if (token.value.kind != 'number') throw new Error();
|
||||
if (token.value.value <= 0) throw new Error();
|
||||
if (Math.trunc(token.value.value) != token.value.value) throw new Error();
|
||||
let result = token.value.value;
|
||||
if (!token.next().done) throw new Error();
|
||||
return result;
|
||||
});
|
||||
sky.registerProperty({
|
||||
name: 'beehive-count',
|
||||
type: BeehiveCountStyleValueType,
|
||||
inherits: true,
|
||||
initialValue: 5,
|
||||
needsLayout: true,
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
div { display: beehive; beehive-count: 3; }
|
||||
</style>
|
||||
<div>
|
||||
<t>Hello</t>
|
||||
<t>World</t>
|
||||
<t>How</t>
|
||||
<t>Are</t>
|
||||
<t>You</t>
|
||||
<t>Today?</t>
|
||||
</div>
|
143
examples/style/sky-core-styles.sky
Normal file
143
examples/style/sky-core-styles.sky
Normal file
|
@ -0,0 +1,143 @@
|
|||
SKY MODULE
|
||||
<!-- this is part of sky:core -->
|
||||
<script>
|
||||
// "internals" is an object only made visible to this module that exports stuff implemented in C++
|
||||
module.exports.registerProperty = internals.registerProperty;
|
||||
internals.registerLayoutManager('none', null);
|
||||
module.exports.LayoutManager = internals.LayoutManager;
|
||||
module.exports.InlineLayoutManager = internals.InlineLayoutManager;
|
||||
internals.registerLayoutManager('inline', internals.InlineLayoutManager);
|
||||
module.exports.ParagraphLayoutManager = internals.ParagraphLayoutManager;
|
||||
internals.registerLayoutManager('paragraph', internals.ParagraphLayoutManager);
|
||||
module.exports.BlockLayoutManager = internals.BlockLayoutManager;
|
||||
internals.registerLayoutManager('block', internals.BlockLayoutManager);
|
||||
|
||||
let displayTypes = new Map();
|
||||
module.exports.registerLayoutManager = function registerLayoutManager(displayValue, layoutManagerConstructor) {
|
||||
// TODO(ianh): apply rules for type-checking displayValue is a String
|
||||
// TODO(ianh): apply rules for type-checking layoutManagerConstructor implements the LayoutManagerConstructor interface (or is null)
|
||||
if (displayTypes.has(displayValue))
|
||||
throw new Error();
|
||||
displayTypes.set(displayValue, layoutManagerConstructor);
|
||||
};
|
||||
|
||||
module.exports.DisplayStyleValueType = new StyleValueType(); // value is null or a LayoutManagerConstructor
|
||||
module.exports.DisplayStyleValueType.addParser((tokens) => {
|
||||
let token = tokens.next();
|
||||
if (token.done)
|
||||
throw new Error();
|
||||
if (token.value.kind != 'identifier')
|
||||
throw new Error();
|
||||
if (!displayTypes.has(token.value.value))
|
||||
throw new Error();
|
||||
return {
|
||||
value: displayTypes.get(token.value.value),
|
||||
}
|
||||
});
|
||||
|
||||
internals.registerProperty({
|
||||
name: 'display',
|
||||
type: module.exports.DisplayStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: internals.BlockLayoutManager,
|
||||
needsLayout: true,
|
||||
});
|
||||
|
||||
module.exports.PositiveLengthStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is a number in 96dpi pixels, >=0
|
||||
module.exports.PositiveLengthStyleValueType.addParser((tokens) => {
|
||||
// just handle "<number>px"
|
||||
let token = tokens.next();
|
||||
if (token.done)
|
||||
throw new Error();
|
||||
if (token.value.kind != 'dimension')
|
||||
throw new Error();
|
||||
if (token.value.unit != 'px')
|
||||
throw new Error();
|
||||
if (token.value.value < 0)
|
||||
throw new Error();
|
||||
return {
|
||||
value: token.value.value;
|
||||
};
|
||||
});
|
||||
|
||||
internals.registerProperty({
|
||||
name: 'min-width',
|
||||
type: module.exports.PositiveLengthStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: 0,
|
||||
needsLayout: true,
|
||||
});
|
||||
internals.registerProperty({
|
||||
name: 'min-height',
|
||||
type: module.exports.PositiveLengthStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: 0,
|
||||
needsLayout: true,
|
||||
});
|
||||
|
||||
module.exports.PositiveLengthOrAutoStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is either a number in 96dpi pixels (>=0) or null (meaning 'auto')
|
||||
module.exports.PositiveLengthOrAutoStyleValueType.addParser((tokens) => {
|
||||
// handle 'auto'
|
||||
let token = tokens.next();
|
||||
if (token.done)
|
||||
throw new Error();
|
||||
if (token.value.kind != 'identifier')
|
||||
throw new Error();
|
||||
if (token.value.value != 'auto')
|
||||
throw new Error();
|
||||
return {
|
||||
value: null,
|
||||
};
|
||||
});
|
||||
module.exports.PositiveLengthOrAutoStyleValueType.addParser((tokens) => {
|
||||
return module.exports.PositiveLengthStyleValueType.parse(tokens);
|
||||
});
|
||||
|
||||
internals.registerProperty({
|
||||
name: 'width',
|
||||
type: module.exports.PositiveLengthOrAutoStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: null,
|
||||
needsLayout: true,
|
||||
});
|
||||
internals.registerProperty({
|
||||
name: 'height',
|
||||
type: module.exporets.PositiveLengthOrAutoStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: null,
|
||||
needsLayout: true,
|
||||
});
|
||||
|
||||
module.exports.PositiveLengthOrInfinityStyleValueType = new StyleValueType(); // value is a ParsedValue whose value (once resolved) is either a number in 96dpi pixels (>=0) or Infinity
|
||||
module.exports.PositiveLengthOrInfinityStyleValueType.addParser((tokens) => {
|
||||
// handle 'infinity'
|
||||
let token = tokens.next();
|
||||
if (token.done)
|
||||
throw new Error();
|
||||
if (token.value.kind != 'identifier')
|
||||
throw new Error();
|
||||
if (token.value.value != 'infinity')
|
||||
throw new Error();
|
||||
return {
|
||||
value: Infinity,
|
||||
};
|
||||
});
|
||||
module.exports.PositiveLengthOrInfinityStyleValueType.addParser((tokens) => {
|
||||
return module.exports.PositiveLengthStyleValueType.parse(tokens);
|
||||
});
|
||||
|
||||
internals.registerProperty({
|
||||
name: 'width',
|
||||
type: module.exports.PositiveLengthOrInfinityStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: Infinity,
|
||||
needsLayout: true,
|
||||
});
|
||||
internals.registerProperty({
|
||||
name: 'height',
|
||||
type: module.exporets.PositiveLengthOrInfinityStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: Infinity,
|
||||
needsLayout: true,
|
||||
});
|
||||
</script>
|
208
examples/style/toolbar-layout.sky
Normal file
208
examples/style/toolbar-layout.sky
Normal file
|
@ -0,0 +1,208 @@
|
|||
SKY MODULE
|
||||
<import src="sky:core" as="sky"/>
|
||||
<script>
|
||||
// display: toolbar;
|
||||
// toolbar-spacing: <length>
|
||||
// display: spring; // remaining space is split equally amongst the springs
|
||||
// children are vertically centered, layout out left-to-right with toolbar-spacing space between them
|
||||
// last child is hidden by default unless there's not enough room for the others, then it's shown last, right-aligned
|
||||
module.exports.SpringLayoutManager = class SpringLayoutManager extends sky.LayoutManager { }
|
||||
sky.registerLayoutManager('spring', module.exports.SpringLayoutManager);
|
||||
sky.registerProperty({
|
||||
name: 'toolbar-spacing',
|
||||
type: sky.LengthStyleValueType,
|
||||
inherits: true,
|
||||
initialValue: { value: 8, unit: 'px' },
|
||||
needsLayout: true,
|
||||
});
|
||||
module.exports.ToolbarLayoutManager = class ToolbarLayoutManager extends sky.LayoutManager {
|
||||
constructor (styleNode) {
|
||||
super(styleNode);
|
||||
this.showingOverflow = false;
|
||||
this.firstSkippedChild = null;
|
||||
this.overflowChild = null;
|
||||
}
|
||||
function layout(width, height) {
|
||||
let children = null;
|
||||
let loop = null;
|
||||
if (height == null)
|
||||
height = this.getIntrinsicHeight().value;
|
||||
if (width == null)
|
||||
this.assumeDimensions(0, height);
|
||||
else
|
||||
this.assumeDimensions(width, height);
|
||||
let spacing = this.node.getProperty('toolbar-spacing');
|
||||
if (typeof spacing != 'number')
|
||||
spacing = 0;
|
||||
this.overflowChild = null;
|
||||
this.firstSkippedChild = null;
|
||||
|
||||
// layout children and figure out whether we need to truncate the child list and show the overflow child
|
||||
let springCount = 0;
|
||||
let minX = 0;
|
||||
let overflowChildWidth = 0;
|
||||
let pendingSpacing = 0;
|
||||
children = this.walkChildren();
|
||||
loop = children.next();
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
let dims = null;
|
||||
if (child.layoutManager instanceof module.exports.SpringLayoutManager) {
|
||||
springCount += 1;
|
||||
pendingSpacing = spacing; // not +=, because we only have one extra spacing per batch of springs
|
||||
} else {
|
||||
if (child.needsLayout) {
|
||||
childHeight = child.layoutManager.getIntrinsicHeight();
|
||||
if (childHeight.value < height)
|
||||
childHeight = childHeight.value;
|
||||
else
|
||||
childHeight = height;
|
||||
dims = child.layoutManager.layout(width, height);
|
||||
this.setChildSize(child, dims.width, dims.height);
|
||||
} else {
|
||||
dims = {
|
||||
width: child.width,
|
||||
height: child.height,
|
||||
};
|
||||
}
|
||||
loop = children.next();
|
||||
if (!loop.done) {
|
||||
if (minX > 0)
|
||||
minX += spacing + pendingSpacing;
|
||||
minX += dims.width;
|
||||
pendingSpacing = 0;
|
||||
} else {
|
||||
overflowChildWidth = spacing + dims.width;
|
||||
this.overflowChild = child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// figure out the spacing
|
||||
this.showingOverflow = false;
|
||||
let springSize = 0;
|
||||
if (width != null) {
|
||||
if (minX <= width) {
|
||||
if (springCount > 0)
|
||||
springSize = (width - minX) / sprintCount;
|
||||
} else {
|
||||
this.showingOverflow = true;
|
||||
}
|
||||
} else {
|
||||
width = minX;
|
||||
}
|
||||
|
||||
// position the children
|
||||
// TODO(ianh): support rtl toolbars
|
||||
let x = 0;
|
||||
let lastWasNonSpring = false;
|
||||
children = this.walkChildren();
|
||||
loop = children.next();
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
if (child.layoutManager instanceof module.exports.SpringLayoutManager) {
|
||||
x += springSize;
|
||||
if (lastWasNonSpring)
|
||||
x += spacing;
|
||||
lastWasNonSpring = false;
|
||||
} else {
|
||||
if (!loop.done) {
|
||||
if (x + child.width + overflowChildWidth > width) {
|
||||
this.firstSkippedChild = child;
|
||||
break; // don't display any more children
|
||||
}
|
||||
this.setChildPosition(child, x, (height - child.height)/2);
|
||||
x += child.width + spacing;
|
||||
lastWasNonSpring = true;
|
||||
} else {
|
||||
// assert: this.showingOverflow == false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.showingOverflow)
|
||||
this.setChildPosition(this.overflowChild, width-this.overflowChild.width, (height - this.overflowChild.height)/2);
|
||||
else
|
||||
this.firstSkippedChild = this.overflowChild;
|
||||
|
||||
return {
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
function getIntrinsicWidth() {
|
||||
let width = this.node.getProperty('width');
|
||||
if (typeof height != 'number') {
|
||||
let spacing = this.node.getProperty('toolbar-spacing');
|
||||
if (typeof spacing != 'number')
|
||||
spacing = 0;
|
||||
width = 0;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
// we exclude the last child because at our ideal width we wouldn't need it
|
||||
let last1 = null; // last one
|
||||
let last2 = null; // one before the last one
|
||||
while (!loop.done) {
|
||||
if (last1)
|
||||
width += last1.layoutManager.getIntrinsicWidth().value;
|
||||
if (last2)
|
||||
width += spacing;
|
||||
last2 = last1;
|
||||
last1 = loop.value;
|
||||
loop = children.next();
|
||||
}
|
||||
}
|
||||
return super(width); // applies and provides our own min-width/max-width rules
|
||||
}
|
||||
function getIntrinsicHeight() {
|
||||
// we grow our minimum height to be no smaller than the children's
|
||||
let result = super();
|
||||
let determineHeight = false;
|
||||
let heightProperty = this.node.getProperty('height');
|
||||
if (typeof heightProperty != 'number')
|
||||
determineHeight = true;
|
||||
let children = this.walkChildren();
|
||||
let loop = children.next();
|
||||
// here we include the last child so that if it pops in our height doesn't change
|
||||
while (!loop.done) {
|
||||
let child = loop.value;
|
||||
let childHeight = child.layoutManager.getIntrinsicHeight();
|
||||
if (determineHeight) {
|
||||
if (result.value < childHeight.value)
|
||||
result.value = childHeight.value;
|
||||
}
|
||||
if (result.minimum < childHeight.minimum)
|
||||
result.minimum = childHeight.minimum;
|
||||
loop = children.next();
|
||||
}
|
||||
if (result.minimum > result.maximum)
|
||||
result.maximum = result.minimum;
|
||||
if (result.value > result.maximum)
|
||||
result.value = result.maximum;
|
||||
if (result.value < result.minimum)
|
||||
result.value = result.minimum;
|
||||
return result;
|
||||
}
|
||||
function paintChildren(canvas) {
|
||||
let width = this.node.width;
|
||||
let loop = children.next();
|
||||
while ((!loop.done) && (loop.value != this.firstSkippedChild))
|
||||
this.paintChild(loop.value, canvas);
|
||||
if (this.showingOverflow)
|
||||
this.paintChild(this.overflowChild, canvas);
|
||||
}
|
||||
function paintChild(child, canvas) {
|
||||
if (child.needsPaint) {
|
||||
canvas.save();
|
||||
try {
|
||||
canvas.beginPath();
|
||||
canvas.rect(child.x, child.y, child.width, child.height);
|
||||
canvas.clip();
|
||||
child.paint(canvas);
|
||||
} finally {
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sky.registerLayoutManager('toolbar', module.exports.ToolbarLayoutManager);
|
||||
</script>
|
Loading…
Reference in a new issue