mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Specs: Initial hack at extensible style/layout
Review URL: https://codereview.chromium.org/716013002
This commit is contained in:
parent
586849aed1
commit
24b06d8998
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>
|
||||
@ -151,17 +151,17 @@ module 'sky:core' {
|
||||
// BUILT-IN ELEMENTS
|
||||
|
||||
class ImportElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "import"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class TemplateElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "template"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
@ -169,25 +169,25 @@ module 'sky:core' {
|
||||
readonly attribute DocumentFragment content; // O(1)
|
||||
}
|
||||
class ScriptElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "script"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class StyleElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "style"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class ContentElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "content"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
@ -195,65 +195,65 @@ module 'sky:core' {
|
||||
Array<Node> getDistributedNodes(); // O(N) in distributed nodes
|
||||
}
|
||||
class ImgElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "img"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class DivElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "div"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class SpanElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "span"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class IframeElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "iframe"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class TElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "t"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class AElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "a"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class TitleElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "title"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
}
|
||||
class ErrorElement : Element {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
constructor attribute String tagName; // O(1) // "error"
|
||||
constructor attribute Boolean shadow; // O(1) // false
|
||||
@ -271,9 +271,9 @@ module 'sky:core' {
|
||||
}
|
||||
|
||||
interface ElementConstructor {
|
||||
constructor (Dictionary attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (Dictionary<String> attributes, ChildArguments... nodes); // O(M+N), M = number of attributes, N = number of nodes plus all their descendants
|
||||
constructor (ChildArguments... nodes); // shorthand
|
||||
constructor (Dictionary attributes); // shorthand
|
||||
constructor (Dictionary<String> attributes); // shorthand
|
||||
constructor (); // shorthand
|
||||
|
||||
constructor attribute String tagName;
|
||||
@ -452,14 +452,16 @@ The following types are available:
|
||||
|
||||
* ``Integer`` - WebIDL ``long long``
|
||||
* ``Float`` - WebIDL ``double``
|
||||
* ``Infinity`` - singleton type with value ``Infinity``
|
||||
* ``String`` - WebIDL ``USVString``
|
||||
* ``Boolean`` - WebIDL ``boolean``
|
||||
# ``Object`` - WebIDL ``object`` (``ClassName`` can be used as a literal for this type)
|
||||
* ``ClassName`` - an instance of the class ClassName
|
||||
* ``DictionaryName`` - an instance of the dictionary DictionaryName
|
||||
* ``Promise<Type>`` - WebIDL ``Promise<T>``
|
||||
* ``Generator<Type>`` - An ECMAScript generator function that returns data of the given type
|
||||
* ``Array<Type>`` - WebIDL ``sequence<T>``
|
||||
* ``Dictionary`` - unordered set of name-value String-String pairs with no duplicate names
|
||||
* ``Dictionary<Type>`` - unordered set of name-value String-Type pairs with no duplicate names
|
||||
* ``Type?`` - union of Type and the singleton type with value ``null`` (WebIDL nullable)
|
||||
* ``(Type1 or Type2)`` - union of Type1 and Type2 (WebIDL union)
|
||||
* ``any`` - union of all types (WebIDL ``any``)
|
||||
|
||||
513
specs/style.md
513
specs/style.md
@ -1,21 +1,510 @@
|
||||
Sky Style Language
|
||||
==================
|
||||
|
||||
For now, the Sky style language is CSS with the following restrictions:
|
||||
|
||||
- No combinators
|
||||
- Only = and ~= attribute selectors
|
||||
- Lots of other selectors removed // TODO(ianh): list them
|
||||
- Floats removed
|
||||
- Lots of other layout models removed // TODO(ianh): list them
|
||||
|
||||
|
||||
Planed changes
|
||||
--------------
|
||||
|
||||
Add //-to-end-of-line comments to be consistent with the script
|
||||
language.
|
||||
|
||||
Add a way to add new values, e.g. by default only support #RRGGBB
|
||||
colours (or maybe only rgba() colours), but provide a way to enable
|
||||
CSS4-like "color(red rgb(+ #004400))" stuff.
|
||||
|
||||
Style Parser
|
||||
------------
|
||||
|
||||
(this section is incomplete)
|
||||
|
||||
### Tokenisation
|
||||
|
||||
|
||||
#### Value parser
|
||||
|
||||
|
||||
##### **Value** state
|
||||
|
||||
If the current character is...
|
||||
|
||||
* '``;``': Consume the character and exit the value parser
|
||||
successfully.
|
||||
|
||||
* '``@``': Consume the character and switch to the **at**
|
||||
state.
|
||||
|
||||
* '``#``': Consume the character and switch to the **hash**
|
||||
state.
|
||||
|
||||
* '``$``': Consume the character and switch to the **dollar**
|
||||
state.
|
||||
|
||||
* '``%``': Consume the character and switch to the **percent**
|
||||
state.
|
||||
|
||||
* '``&``': Consume the character and switch to the **ampersand**
|
||||
state.
|
||||
|
||||
* '``'``': Set _value_ to the empty string, consume the character, and
|
||||
switch to the **single-quoted string** state.
|
||||
|
||||
* '``"``': Set _value_ to the empty string, consume the character, and
|
||||
switch to the **double-quoted string** state.
|
||||
|
||||
* '``-``': Consume the character, and switch to the **negative
|
||||
integer** state.
|
||||
|
||||
* '``0``'-'``9``': Set _value_ to the decimal value of the current
|
||||
character, consume the character, and switch to the **integer**
|
||||
state.
|
||||
|
||||
* '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to the current
|
||||
character, consume the character, and switch to the **identifier**
|
||||
state.
|
||||
|
||||
* '``*``', '``^``', '``!``', '``?``', '``,``', '``/``', '``<``',
|
||||
'``[``', '``)``', '``>``', '``]``', '``+``': Emit a symbol token
|
||||
with the current character as the symbol, consume the character, and
|
||||
stay in this state.
|
||||
|
||||
* Anything else: Consume the character and switch to the **error**
|
||||
state.
|
||||
|
||||
|
||||
##### **At** state
|
||||
|
||||
* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
|
||||
the current character, create a literal token with the unit set to
|
||||
``@``, consume the character, and switch to the **literal** state.
|
||||
|
||||
* Anything else: Emit a symbol token with ``@`` as the symbol, and
|
||||
switch to the **value** state without consuming the character.
|
||||
|
||||
|
||||
##### **Hash** state
|
||||
|
||||
* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
|
||||
the current character, create a literal token with the unit set to
|
||||
``@``, consume the character, and switch to the **literal** state.
|
||||
|
||||
* Anything else: Emit a symbol token with ``#`` as the symbol, and
|
||||
switch to the **value** state without consuming the character.
|
||||
|
||||
|
||||
##### **Dollar** state
|
||||
|
||||
* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
|
||||
the current character, create a literal token with the unit set to
|
||||
``@``, consume the character, and switch to the **literal** state.
|
||||
|
||||
* Anything else: Emit a symbol token with ``$`` as the symbol, and
|
||||
switch to the **value** state without consuming the character.
|
||||
|
||||
|
||||
##### **Percent** state
|
||||
|
||||
* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
|
||||
the current character, create a literal token with the unit set to
|
||||
``@``, consume the character, and switch to the **literal** state.
|
||||
|
||||
* Anything else: Emit a symbol token with ``%`` as the symbol, and
|
||||
switch to the **value** state without consuming the character.
|
||||
|
||||
|
||||
##### **Ampersand** state
|
||||
|
||||
* '``0``'-'``9``', '``a``'-'``z``', '``A``'-'``Z``': Set _value_ to
|
||||
the current character, create a literal token with the unit set to
|
||||
``@``, consume the character, and switch to the **literal** state.
|
||||
|
||||
* Anything else: Emit a symbol token with ``&`` as the symbol, and
|
||||
switch to the **value** state without consuming the character.
|
||||
|
||||
|
||||
##### TODO(ianh): more states...
|
||||
|
||||
|
||||
##### **Error** state
|
||||
|
||||
If the current character is...
|
||||
|
||||
* '``;``': Consume the character and exit the value parser in failure.
|
||||
|
||||
* Anything else: Consume the character and stay in this state.
|
||||
|
||||
|
||||
|
||||
Selectors
|
||||
---------
|
||||
|
||||
Sky Style uses whatever SelectorQuery. Maybe one day we'll make
|
||||
SelectorQuery support being extended to support arbitrary selectors,
|
||||
but for now, it supports:
|
||||
|
||||
tagname
|
||||
#id
|
||||
.class
|
||||
[attrname]
|
||||
[attrname=value]
|
||||
:host
|
||||
|
||||
These can be combined (without whitespace), with at most one tagname,
|
||||
as in:
|
||||
|
||||
tagname[attrname]#id:host.class.class[attrname=value]
|
||||
|
||||
In debug mode, giving two IDs, or the same selector twice (e.g. the
|
||||
same classname), or specifying other redundant or conflicting
|
||||
selectors (e.g. [foo][foo=bar], or [foo=bar][foo=baz]) will be
|
||||
flagged.
|
||||
|
||||
Alternatively, a selector can be the following special value:
|
||||
|
||||
@document
|
||||
|
||||
|
||||
Value Parser
|
||||
------------
|
||||
|
||||
class StyleToken {
|
||||
constructor (String king, String value);
|
||||
readonly attribute String kind;
|
||||
// string
|
||||
// identifier
|
||||
// function (identifier + '(')
|
||||
// number
|
||||
// symbol (one of @#$%& if not immediately following numeric or preceding alphanumeric, or one of *^!?,/<[)>]+ or, if not followed by a digit, -)
|
||||
// dimension (number + identifier or number + one of @#$%&)
|
||||
// literal (one of @#$%& + alphanumeric)
|
||||
readonly attribute String value;
|
||||
readonly attribute String unit; // for 'dimension' type, this is the punctuation or identifier that follows the number, for 'literal' type, this is the punctuation that precedes it
|
||||
}
|
||||
|
||||
class TokenSource {
|
||||
constructor (Array<StyleToken> tokens);
|
||||
IteratorResult next();
|
||||
TokenSourceBookmark getBookmark();
|
||||
void rewind(TokenSourceBookmark bookmark);
|
||||
}
|
||||
class TokenSourceBookmark {
|
||||
constructor ();
|
||||
// TokenSource stores unforgeable state on this object using symbols or a weakmap or some such
|
||||
}
|
||||
|
||||
dictionary ParsedValue {
|
||||
any value = null;
|
||||
ValueResolver? resolver = null;
|
||||
Boolean relativeDimension = false; // if true, e.g. for % lengths, the callback will be called again if an ancestor's dimensions change
|
||||
Painter? painter = null;
|
||||
}
|
||||
|
||||
// best practice convention: if you're creating a property with needsPaint, you should
|
||||
// create a new style value type for it so that it can set the paint callback right;
|
||||
// you should never use such a style type when parsing another property
|
||||
|
||||
callback any ParserCallback (TokenSource tokens);
|
||||
|
||||
class StyleValueType {
|
||||
constructor ();
|
||||
void addParser(ParserCallback parser);
|
||||
any parse(TokenSource tokens, Boolean root = false);
|
||||
// for each parser callback that was registered, in reverse
|
||||
// order (most recently registered first), run these steps:
|
||||
// let bookmark = tokens.getBookmark();
|
||||
// try {
|
||||
// let result = parser(tokens);
|
||||
// if (root) {
|
||||
// if (!tokens.next().done)
|
||||
// throw new Error();
|
||||
// }
|
||||
// } except {
|
||||
// tokens.rewind(bookmark);
|
||||
// }
|
||||
// (root is set when you need to parse the entire token stream to be valid)
|
||||
}
|
||||
|
||||
// note: if you define a style value type that uses other style value types, e.g. a "length pair" that accepts two lengths, then
|
||||
// if any of the subtypes have a resolver, you need to make sure you have a resolver that calls them to compute the final value
|
||||
|
||||
dictionary PropertySettings {
|
||||
String name;
|
||||
StyleValueType type; // the output from the parser is coerced to a ParsedValue
|
||||
Boolean inherits = false;
|
||||
any initialValue = null;
|
||||
Boolean needsLayout = false;
|
||||
Boolean needsPaint = false;
|
||||
}
|
||||
|
||||
void registerProperty(PropertySettings propertySettings);
|
||||
// when you register a new property, document the format that is expected to be cascaded
|
||||
// (the output from the propertySettings.type parser's ParsedValue.value field after the resolver, if any, has been called)
|
||||
|
||||
// sky:core exports a bunch of style value types so that people can
|
||||
// extend them
|
||||
attribute StyleValueType PositiveLengthOrInfinityStyleValueType;
|
||||
attribute StyleValueType PositiveLengthOrAutoStyleValueType;
|
||||
attribute StyleValueType PositiveLengthStyleValueType;
|
||||
attribute StyleValueType DisplayStyleValueType;
|
||||
|
||||
|
||||
Inline Styles
|
||||
-------------
|
||||
|
||||
partial class Element {
|
||||
readonly attribute StyleDeclarationList style;
|
||||
}
|
||||
|
||||
class StyleDeclarationList {
|
||||
constructor ();
|
||||
void add(StyleDeclaration styles); // O(1) // in debug mode, throws if the dictionary has any properties that aren't registered
|
||||
void remove(StyleDeclaration styles); // O(N) in number of declarations
|
||||
Array<StyleDeclaration> getDeclarations(); // O(N) in number of declarations
|
||||
}
|
||||
|
||||
typedef StyleDeclaration Dictionary<ParsedValue>;
|
||||
|
||||
|
||||
Rule Matching
|
||||
-------------
|
||||
|
||||
partial class StyleElement {
|
||||
Array<Rule> getRules(); // O(N) in rules
|
||||
}
|
||||
|
||||
class Rule {
|
||||
constructor ();
|
||||
attribute SelectorQuery selector; // O(1)
|
||||
attribute StyleDeclaration styles; // O(1)
|
||||
}
|
||||
|
||||
Each frame, at some defined point relative to requestAnimationFrame():
|
||||
- If a rule starts applying to an element, sky:core calls thatElement.style.add(rule.styles);
|
||||
- If a rule stops applying to an element, sky:core calls thatElement.style.remove(rule.styles);
|
||||
|
||||
|
||||
Cascade
|
||||
-------
|
||||
|
||||
For each Element, the StyleDeclarationList is conceptually flattened
|
||||
so that only the last declaration mentioning a property is left.
|
||||
|
||||
Create the flattened render tree as a tree of StyleNode objects
|
||||
(described below). For each one, run the equivalent of the following
|
||||
code:
|
||||
|
||||
var display = node.getProperty('display');
|
||||
if (display) {
|
||||
node.layoutManager = new display(node, ownerManager);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
If that code returns false, then that node an all its descendants must
|
||||
be dropped from the render tree.
|
||||
|
||||
If any node is removed in this pass relative to the previous pass, and
|
||||
it has an ownerLayoutManager, then call
|
||||
``node.ownerLayoutManager.release(node)``
|
||||
...to notify the layout manager that the node went away, then set the
|
||||
node's layoutManager and ownerLayoutManager attributes to null.
|
||||
|
||||
callback any ValueResolver (any value, String propertyName, StyleNode node, Float containerWidth, Float containerHeight);
|
||||
|
||||
class StyleNode {
|
||||
// this is generated before layout
|
||||
readonly attribute String text;
|
||||
readonly attribute Node? parentNode;
|
||||
readonly attribute Node? firstChild;
|
||||
readonly attribute Node? nextSibling;
|
||||
|
||||
// access to the results of the cascade
|
||||
any getProperty(String name);
|
||||
// if there's a cached value, return it
|
||||
// otherwise, if there's an applicable ParsedValue, then
|
||||
// if it has a resolver:
|
||||
// call it
|
||||
// cache the value
|
||||
// if relativeDimension is true, then mark the value as provisional
|
||||
// return the value
|
||||
// otherwise use the ParsedValue's value; cache it; return it
|
||||
// otherwise, if the property is inherited and there's a parent:
|
||||
// get it from the parent; cache it; return it
|
||||
// otherwise, get the default value; cache it; return it
|
||||
|
||||
readonly attribute Boolean needsLayout; // means that a property with needsLayout:true has changed on this node or one of its descendants
|
||||
readonly attribute LayoutManager layoutManager;
|
||||
|
||||
readonly attribute LayoutManager ownerLayoutManager; // defaults to the parentNode.layoutManager
|
||||
// if you are not the ownerLayoutManager, then ignore this StyleNode in layout() and paintChildren()
|
||||
// using walkChildren() does this for you
|
||||
|
||||
readonly attribute Boolean needsPaint; // means that either needsLayout is true or a property with needsPaint:true has changed on this node or one of its descendants
|
||||
|
||||
// only the ownerLayoutManager can change these
|
||||
readonly attribute Float x;
|
||||
readonly attribute Float y;
|
||||
readonly attribute Float width;
|
||||
readonly attribute Float height;
|
||||
}
|
||||
|
||||
The flattened tree is represented as a hierarchy of Node objects. For
|
||||
any element that only contains text node children, the "text" property
|
||||
is set accordingly. For elements with mixed text node and non-text
|
||||
node children, each run of text nodes is represented as a separate
|
||||
Node with the "text" property set accordingly and the styles set as if
|
||||
the Node inherited everything inheritable from its parent.
|
||||
|
||||
|
||||
Layout
|
||||
------
|
||||
|
||||
sky:core registers 'display' as follows:
|
||||
|
||||
{
|
||||
name: 'display',
|
||||
type: sky.DisplayStyleValueType,
|
||||
inherits: false,
|
||||
initialValue: sky.BlockLayoutManager,
|
||||
needsLayout: true,
|
||||
}
|
||||
|
||||
The following API is then used to add new layout manager types to 'display':
|
||||
|
||||
void registerLayoutManager(String displayValue, LayoutManagerConstructor? layoutManager);
|
||||
|
||||
sky:core by default registers:
|
||||
|
||||
'block': sky.BlockLayoutManager
|
||||
'paragraph': sky.ParagraphLayoutManager
|
||||
'inline': sky.InlineLayoutManager
|
||||
'none': null
|
||||
|
||||
|
||||
Layout managers inherit from the following API:
|
||||
|
||||
class LayoutManager {
|
||||
readonly attribute StyleNode node;
|
||||
constructor LayoutManager(StyleNode node);
|
||||
|
||||
void take(StyleNode victim); // sets victim.ownerLayoutManager = this;
|
||||
// assert: victim hasn't been take()n yet during this layout
|
||||
// assert: victim.needsLayout == true
|
||||
// assert: an ancestor of victim has needsLayout == this (aka, victim is a descendant of this.node)
|
||||
|
||||
virtual void release(StyleNode victim);
|
||||
// called when the StyleNode was removed from the tree
|
||||
|
||||
void setChildPosition(child, x, y); // sets child.x, child.y
|
||||
void setChildX(child, y); // sets child.x
|
||||
void setChildY(child, y); // sets child.y
|
||||
void setChildSize(child, width, height); // sets child.width, child.height
|
||||
void setChildWidth(child, width); // sets child.width
|
||||
void setChildHeight(child, height); // sets child.height
|
||||
// these set needsPaint on the node and on any node impacted by this (?)
|
||||
// for setChildSize/Width/Height: if the new dimension is different than the last assumed dimensions, and
|
||||
// any StyleNodes with an ownerLayoutManager==this have cached values for getProperty() that are marked
|
||||
// as provisional, clear them
|
||||
|
||||
Generator<StyleNode> walkChildren();
|
||||
// returns a generator that iterates over the children, skipping any whose ownerLayoutManager is not this
|
||||
|
||||
void paint(RenderingSurface canvas);
|
||||
// set a clip rect on the canvas
|
||||
// call the painter of each property, in order they were registered, which on this element has a painter
|
||||
// call this.paintChildren()
|
||||
// unset the clip
|
||||
|
||||
virtual void paintChildren(RenderingSurface canvas);
|
||||
// just calls paint() for each child returned by walkChildren() whose needsPaint is true
|
||||
|
||||
void assumeDimensions(Float width, Float height);
|
||||
// sets the assumed dimensions for calls to getProperty() on StyleNodes that have this as an ownerLayoutManager
|
||||
// if the new dimension is different than the last assumed dimensions, and any StyleNodes with an
|
||||
// ownerLayoutManager==this have cached values for getProperty() that are marked as provisional, clear them
|
||||
// TODO(ianh): should we force this to match the input to layout(), when called from inside layout() and when
|
||||
// layout() has a forced width and/or height?
|
||||
|
||||
virtual LayoutValueRange getIntrinsicWidth(Float? defaultWidth = null);
|
||||
// returns min-width, width, and max-width, normalised, defaulting to values given in LayoutValueRange
|
||||
// if argument is provided, it overrides width
|
||||
|
||||
virtual LayoutValueRange getIntrinsicHeight(Float? defaultHeight = null);
|
||||
// returns min-height, height, and max-height, normalised, defaulting to values given in LayoutValueRange
|
||||
// if argument is provided, it overrides height
|
||||
|
||||
virtual Dimensions layout(Number? width, Number? height);
|
||||
// returns { }
|
||||
// the return value should include the final value for whichever of the width and height arguments that is null
|
||||
// TODO(ianh): should we just grab the width and height from assumeDimensions()?
|
||||
|
||||
}
|
||||
|
||||
dictionary LayoutValueRange {
|
||||
// negative values here should be treated as zero
|
||||
Float minimum = 0;
|
||||
Float value = 0; // ideal desired width; if it's not in the range minimum .. maximum then it overrides minimum and maximum
|
||||
(Float or Infinity) maximum = Infinity;
|
||||
}
|
||||
|
||||
dictionary Dimensions {
|
||||
Float width = 0;
|
||||
Float height = 0;
|
||||
}
|
||||
|
||||
|
||||
Given a tree of StyleNode objects rooted at /node/, the application is
|
||||
rendered as follows:
|
||||
|
||||
node.layoutManager.layout(screen.width, screen.height);
|
||||
node.layoutManager.paint();
|
||||
|
||||
|
||||
|
||||
Paint
|
||||
-----
|
||||
|
||||
callback void Painter (StyleNode node, RenderingSurface canvas);
|
||||
|
||||
class RenderingSurface {
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
Default Styles
|
||||
--------------
|
||||
|
||||
In the constructors for the default elements, they add to themselves
|
||||
StyleDeclaration objects as follows:
|
||||
|
||||
* ``import``
|
||||
* ``template``
|
||||
* ``style``
|
||||
* ``script``
|
||||
* ``content``
|
||||
* ``title``
|
||||
These all add to themselves the same declaration with value:
|
||||
``{ display: { value: null } }``
|
||||
|
||||
* ``img``
|
||||
This adds to itself the declaration with value:
|
||||
``{ display: { value: sky.ImageElementLayoutManager } }``
|
||||
|
||||
* ``span``
|
||||
* ``a``
|
||||
These all add to themselves the same declaration with value:
|
||||
``{ display: { value: sky.InlineLayoutManager } }``
|
||||
|
||||
* ``iframe``
|
||||
This adds to itself the declaration with value:
|
||||
``{ display: { value: sky.IFrameElementLayoutManager } }``
|
||||
|
||||
* ``t``
|
||||
This adds to itself the declaration with value:
|
||||
``{ display: { value: sky.ParagraphLayoutManager } }``
|
||||
|
||||
* ``error``
|
||||
This adds to itself the declaration with value:
|
||||
``{ display: { value: sky.ErrorLayoutManager } }``
|
||||
|
||||
The ``div`` element doesn't have any default styles.
|
||||
|
||||
These declarations are all shared between all the elements (so e.g. if
|
||||
you reach in and change the declaration that was added to a ``title``
|
||||
element, you're going to change the styles of all the other
|
||||
default-hidden elements).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user