Specs: Initial hack at extensible style/layout

Review URL: https://codereview.chromium.org/716013002
This commit is contained in:
Hixie 2014-11-13 14:00:46 -08:00
parent af585507a0
commit f8340b77ef
4 changed files with 571 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>