From 160c7df1ca81869b7152ab33771b5a69d73b393d Mon Sep 17 00:00:00 2001 From: Robin Schreiber Date: Tue, 28 Jan 2025 16:27:27 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8E=A8:=20prevent=20generation=20of?= =?UTF-8?q?=20conflicting=20props=20during=20policy=20synthesization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conflicting properties such as width, height, extent or textString, value and textAndAttributes are prioritized and weeded out during synthesization in order to avoid confusing behavior. --- lively.morphic/components/policy.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lively.morphic/components/policy.js b/lively.morphic/components/policy.js index d4b71e68c6..a4f07dc903 100644 --- a/lively.morphic/components/policy.js +++ b/lively.morphic/components/policy.js @@ -81,6 +81,10 @@ export function replace (replacedSiblingName, props) { function handleTextProps (props) { if (arr.intersect( ['text', 'label', Text, Label], withSuperclasses(props.type)).length === 0) { return props; } + if (props.textAndAttributes) { + delete props.textString; + delete props.value; + } if (props.textString) { props.textAndAttributes = [props.textString, null]; delete props.textString; @@ -1391,11 +1395,21 @@ export class PolicyApplicator extends StylePolicy { } synthesizeSubSpec (submorphNameInPolicyContext, parentOfScope, previousTarget) { - const subSpec = super.synthesizeSubSpec(submorphNameInPolicyContext, parentOfScope, previousTarget); - if (subSpec.isPolicy && !subSpec.isPolicyApplicator) { - return new PolicyApplicator({}, subSpec); + let synthesized = super.synthesizeSubSpec(submorphNameInPolicyContext, parentOfScope, previousTarget); + if (synthesized.isPolicy && !synthesized.isPolicyApplicator) { + return new PolicyApplicator({}, synthesized); + } + synthesized = sanitizeSpec(synthesized); + if ('width' in synthesized && synthesized.extent?.isPoint) { + synthesized.extent = synthesized.extent.withX(synthesized.width); + delete synthesized.width; + } + + if ('height' in synthesized && synthesized.extent?.isPoint) { + synthesized.extent = synthesized.extent.withY(synthesized.height); + delete synthesized.height; } - return subSpec; + return synthesized; } applyIfNeeded (needsUpdate = false, animationConfig = false) { From ecc090095c7635f4e7f4f2f57ca1c30ae418745a Mon Sep 17 00:00:00 2001 From: Robin Schreiber Date: Tue, 28 Jan 2025 18:08:31 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=A8:=20improve=20performance=20of?= =?UTF-8?q?=20frequent=20checks=20during=20policy=20application?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lively.morphic/components/policy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lively.morphic/components/policy.js b/lively.morphic/components/policy.js index a4f07dc903..90a397ba7e 100644 --- a/lively.morphic/components/policy.js +++ b/lively.morphic/components/policy.js @@ -1277,7 +1277,7 @@ export class StylePolicy { */ isPositionedByLayout (aSubmorph) { const layout = aSubmorph.owner?.layout; - return layout?.name() !== 'Constraint' && layout?.layoutableSubmorphs?.includes(aSubmorph); + return layout && layout.name() !== 'Constraint' && aSubmorph.isLayoutable; } /** @@ -1486,7 +1486,7 @@ export class PolicyApplicator extends StylePolicy { continue; } - if (this.isPositionedByLayout(morphToBeStyled) && propName === 'position') continue; + if (propName === 'position' && this.isPositionedByLayout(morphToBeStyled)) continue; let resizePolicy; if (propName === 'extent' && (resizePolicy = this.isResizedByLayout(morphToBeStyled))) { if (resizePolicy.widthPolicy === 'fixed' && morphToBeStyled.width !== propValue.x) morphToBeStyled.width = propValue.x; From e54d6932fb9597e5802d3ccd9e6f92a8283f190a Mon Sep 17 00:00:00 2001 From: Robin Schreiber Date: Tue, 28 Jan 2025 16:29:35 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=8E=A8:=20fix=20breakpoint=20update?= =?UTF-8?q?=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the check would give incorrect results when nested breakpoints where being applied. A nested breakpoint is a breakpoint defined by a master that was *itself* reached by a breakpoint. --- lively.morphic/components/policy.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lively.morphic/components/policy.js b/lively.morphic/components/policy.js index 90a397ba7e..e6a8ed5449 100644 --- a/lively.morphic/components/policy.js +++ b/lively.morphic/components/policy.js @@ -471,8 +471,17 @@ export class StylePolicy { })(); } + getLastMatchingBreakpoint (target) { + let curr = this; + let matchingBreakpoint; + while (curr = curr.getMatchingBreakpointMaster(target)) { + matchingBreakpoint = curr = curr.stylePolicy || curr; + } + return matchingBreakpoint || curr; + } + needsBreakpointUpdate (target) { - const matchingBreakpoint = this.getMatchingBreakpointMaster(target); + const matchingBreakpoint = this.getLastMatchingBreakpoint(target); if (typeof matchingBreakpoint === 'undefined') return false; if (matchingBreakpoint === (target._lastBreakpoint || null)) { return false; } target._lastBreakpoint = matchingBreakpoint; From 185a30703193709fc95eada62f35c6067db91488 Mon Sep 17 00:00:00 2001 From: Robin Schreiber Date: Tue, 28 Jan 2025 18:53:18 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=8E=A8:=20refactor=20layout-policy=20?= =?UTF-8?q?resizing=20and=20prevent=20feedbackloop=20via=20meta=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces old and not very well working control flows with a more sophisticated approach to determining sizing behavior of a styled morph within the context of various layouts. According to the identified sizing behavior, the extent is applied in varying fashions. Also previously accidental re-execution of layouts would happen due to policies applying themselves, which lead to inefficient layout applications overall. This is now prevented with a meta flag. --- lively.morphic/components/policy.js | 49 +++++++++++++++-------------- lively.morphic/layout.js | 4 +++ 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/lively.morphic/components/policy.js b/lively.morphic/components/policy.js index e6a8ed5449..943ec26e3e 100644 --- a/lively.morphic/components/policy.js +++ b/lively.morphic/components/policy.js @@ -1295,7 +1295,7 @@ export class StylePolicy { * @returns { boolean | object } Wether or not size is controlled via layout and if so, the concrete policy. */ isResizedByLayout (aSubmorph) { - const layout = aSubmorph.owner && aSubmorph.owner.layout; + let layout = aSubmorph.owner && aSubmorph.owner.layout; let heightPolicy = 'fixed'; let widthPolicy = 'fixed'; if (aSubmorph.isText) { if (!aSubmorph.fixedHeight) heightPolicy = 'hug'; @@ -1306,6 +1306,19 @@ export class StylePolicy { if (widthPolicy !== 'hug') widthPolicy = layout.getResizeWidthPolicyFor(aSubmorph); if (heightPolicy === 'fill' || widthPolicy === 'fill') return { widthPolicy, heightPolicy }; } + + layout = aSubmorph.layout; + + if (layout?.hugContentsVertically || + layout?.hugContentsHorizontally || + widthPolicy === 'hug' || + heightPolicy === 'hug') { + return { + widthPolicy: layout?.hugContentsHorizontally ? 'hug' : widthPolicy, + heightPolicy: layout?.hugContentsVertically ? 'hug' : heightPolicy + }; + } + return false; } @@ -1498,8 +1511,17 @@ export class PolicyApplicator extends StylePolicy { if (propName === 'position' && this.isPositionedByLayout(morphToBeStyled)) continue; let resizePolicy; if (propName === 'extent' && (resizePolicy = this.isResizedByLayout(morphToBeStyled))) { - if (resizePolicy.widthPolicy === 'fixed' && morphToBeStyled.width !== propValue.x) morphToBeStyled.width = propValue.x; - if (resizePolicy.heightPolicy === 'fixed' && morphToBeStyled.height !== propValue.y) morphToBeStyled.height = propValue.y; + morphToBeStyled.withMetaDo({ deferLayoutApplication: true }, () => { + if (resizePolicy.widthPolicy === 'fixed' && morphToBeStyled.width !== propValue.x) { + morphToBeStyled.width = propValue.x; + } + if (resizePolicy.heightPolicy === 'fixed' && morphToBeStyled.height !== propValue.y) { + morphToBeStyled.height = propValue.y; + } + if (morphToBeStyled.isText && (resizePolicy.widthPolicy === 'hug' || resizePolicy.heightPolicy === 'hug')) { + morphToBeStyled.withMetaDo({ doNotFit: false }, () => morphToBeStyled.fit()); + } + }); continue; } @@ -1514,32 +1536,11 @@ export class PolicyApplicator extends StylePolicy { if (propName === 'position') continue; } - // FIXME: other special cases?? - if (morphToBeStyled.isText && propName === 'extent') { - if (!morphToBeStyled.fixedWidth && !morphToBeStyled.fixedHeight) continue; - if (!morphToBeStyled.fixedWidth) propValue = propValue.withX(morphToBeStyled.width); - if (!morphToBeStyled.fixedHeight) propValue = propValue.withY(morphToBeStyled.height); - } - - if (morphToBeStyled.isText && propName === 'width' && morphToBeStyled.lineWrapping !== 'no-wrap') { - if (!morphToBeStyled.fixedWidth) continue; - morphToBeStyled.width = propValue; - morphToBeStyled.withMetaDo({ doNotFit: false }, () => morphToBeStyled.fit()); - } - if (['border', 'borderTop', 'borderBottom', 'borderRight', 'borderLeft'].includes(propName)) continue; // handled by sub props; if (!obj.equals(morphToBeStyled[propName], propValue)) { morphToBeStyled[propName] = propValue; } - - // we may be late for the game when setting these props - // se we need to make sure, we restore the morphs "intended extent" - // for this purpose we enforce the masterSubmorph extent - if (['fixedHeight', 'fixedWidth'].includes(propName) && - morphToBeStyled._parametrizedProps?.extent) { - morphToBeStyled.extent = morphToBeStyled._parametrizedProps.extent; - } } } diff --git a/lively.morphic/layout.js b/lively.morphic/layout.js index 7efb533377..6c5eda505f 100644 --- a/lively.morphic/layout.js +++ b/lively.morphic/layout.js @@ -419,6 +419,10 @@ export class TilingLayout extends Layout { } scheduleApply (submorph, animation, change = {}) { + if (change.meta?.deferLayoutApplication) { + return; + } + if (!change.meta?.isLayoutAction || !this.container?._yogaNode?.getParent()) { this._alreadyComputed = false; } From d42c366598a88241adcc79c37b3667f24a1f3999 Mon Sep 17 00:00:00 2001 From: Robin Schreiber Date: Tue, 28 Jan 2025 19:36:34 +0100 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=8E=A8:=20avoid=20overriding=20extent?= =?UTF-8?q?=20prop=20during=20layout=20application?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously some layout applications, including text layout, would cause extent, height or width properies in style policies to no longer apply, although not warranted. --- lively.morphic/components/policy.js | 8 +++++++- lively.morphic/layout.js | 10 +++++----- lively.morphic/text/morph.js | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/lively.morphic/components/policy.js b/lively.morphic/components/policy.js index 943ec26e3e..d775eaf861 100644 --- a/lively.morphic/components/policy.js +++ b/lively.morphic/components/policy.js @@ -454,7 +454,12 @@ export class StylePolicy { if (targetMorph._lastIndex && !obj.equals(targetMorph._lastIndex, currIndex)) { const limitExtent = bpStore.getLimitExtent(currIndex); const actualExtent = targetMorph.extent; - targetMorph.withMetaDo({ metaInteraction: true, reconcileChanges: false, doNotFit: true }, () => { + targetMorph.withMetaDo({ + metaInteraction: true, // do not record + reconcileChanges: false, + doNotFit: true, + doNotOverride: true + }, () => { const origLayoutableFlag = targetMorph.isLayoutable; targetMorph.isLayoutable = false; // avoid any resizing interference here targetMorph.extent = limitExtent; @@ -1574,6 +1579,7 @@ export class PolicyApplicator extends StylePolicy { */ onMorphChange (changedMorph, change) { if (change.meta?.metaInteraction || + change.meta?.doNotOverride || !this.targetMorph || this.isCurrentlyAnimated(changedMorph) ) return; diff --git a/lively.morphic/layout.js b/lively.morphic/layout.js index 6c5eda505f..86d619dcd6 100644 --- a/lively.morphic/layout.js +++ b/lively.morphic/layout.js @@ -834,10 +834,10 @@ export class TilingLayout extends Layout { aMorph._yogaNode = Yoga.Node.create(yogaConfig); if (aMorph.isText) { aMorph._yogaNode.setMeasureFunc((width, widthMode, height, heightMode) => { - if (aMorph.fixedWidth && widthMode !== 0) aMorph.width = width; - if (aMorph.fixedHeight && heightMode !== 0) aMorph.height = height; + if (aMorph.fixedWidth && widthMode !== 0) aMorph.withMetaDo({ doNotOverride: true }, () => aMorph.width = width); + if (aMorph.fixedHeight && heightMode !== 0) aMorph.withMetaDo({ doNotOverride: true }, () => aMorph.height = height); if (!aMorph.visible) return { width: aMorph.width, height: aMorph.height }; - if (!aMorph.fixedWidth || !aMorph.fixedHeight) aMorph.withMetaDo({ doNotFit: false }, () => aMorph.fit()); + if (!aMorph.fixedWidth || !aMorph.fixedHeight) aMorph.withMetaDo({ doNotFit: false, skipRerender: true }, () => aMorph.fitIfNeeded()); if (!aMorph.fixedWidth) width = aMorph.width; if (!aMorph.fixedHeight) height = aMorph.height; return { width, height }; @@ -934,10 +934,10 @@ export class TilingLayout extends Layout { if (this.container.submorphs.length > 0) { if (hugContentsVertically && container.height !== height) { - container.withMetaDo({ isLayoutAction: true, skipRender: true }, () => container.height = height); + container.withMetaDo({ isLayoutAction: true, skipRender: true, doNotOverride: true }, () => container.height = height); } if (hugContentsHorizontally && container.width !== width) { - container.withMetaDo({ isLayoutAction: true, skipRender: true }, () => container.width = width); + container.withMetaDo({ isLayoutAction: true, skipRender: true, doNotOverride: true }, () => container.width = width); } } } diff --git a/lively.morphic/text/morph.js b/lively.morphic/text/morph.js index 34288210c5..7b5c0dc3e2 100644 --- a/lively.morphic/text/morph.js +++ b/lively.morphic/text/morph.js @@ -2826,8 +2826,7 @@ export class Text extends Morph { } else if (this.env.renderer) { if (this.fixedHeight && this.fixedWidth) return; let textBoundsExtent = this.textBounds().extent(); - this.renderingState.needsFit = this.renderingState.needsRemeasure; - this.withMetaDo({ isLayoutAction: true, doNotFit: true }, () => { + this.withMetaDo({ isLayoutAction: true, doNotFit: true, doNotOverride: true }, () => { if (this.fixedWidth) textBoundsExtent = textBoundsExtent.withX(this.width); if (this.fixedHeight) textBoundsExtent = textBoundsExtent.withY(this.height); const newExt = textBoundsExtent.addXY( @@ -2836,6 +2835,7 @@ export class Text extends Morph { ); if (!this.extent.equals(newExt)) { this.extent = newExt; + this.renderingState.needsFit = this.renderingState.needsRemeasure; } }); } else {