From f41c1163652b88ecdb9e01b6e7c1fe540c0b1b18 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:04:55 +0800 Subject: [PATCH 1/2] refresh the tooltip cache when placeholders are updated --- .../guide/document/block/LytAlignedBlock.java | 3 +- .../guidenh/guide/document/block/LytBox.java | 8 +++--- .../document/block/LytCodeBlockToolbar.java | 1 - .../document/block/LytContentTabsBlock.java | 4 +-- .../guide/document/block/LytDetailsBlock.java | 5 +--- .../guide/document/block/LytDocument.java | 1 + .../document/block/LytDocumentFloat.java | 3 +- .../guidenh/guide/document/block/LytNode.java | 25 +++++++++++++++++ .../guide/document/block/LytParagraph.java | 2 ++ .../document/block/LytPlaceholderBlock.java | 9 +++--- .../guide/document/flow/LytFlowContent.java | 12 ++++++++ .../document/flow/LytFlowInlineBlock.java | 13 ++++++++- .../guide/document/flow/LytFlowSpan.java | 1 + .../document/interaction/ContentTooltip.java | 28 ++++++++++++++++++- .../guidenh/guide/internal/GuideScreen.java | 14 ++++++++-- .../internal/editor/SceneEditorScreen.java | 10 +++++-- .../internal/host/ScriptContextImpl.java | 11 ++------ .../internal/welcome/GuideWelcomeScreen.java | 10 +++++-- .../MediaWikiSpecialGeneratedBlock.java | 10 ++----- .../guide/scene/LytGuidebookScene.java | 5 +--- 20 files changed, 124 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java index b36528e8..b0a5c005 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytAlignedBlock.java @@ -49,8 +49,7 @@ public void replaceChild(LytNode oldChild, LytNode newChild) { inner.parent = null; inner = (LytBlock) newChild; inner.parent = this; - LytDocument doc = getDocument(); - if (doc != null) doc.invalidateLayout(); + invalidateLayout(); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java index a2c7030a..eeb83b38 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytBox.java @@ -36,6 +36,7 @@ public void removeChild(LytNode node) { if (isAttached()) LytDocument.notifyDetach(block); children.remove(block); block.parent = null; + invalidateLayout(); } } @@ -47,6 +48,7 @@ public void append(LytBlock block) { block.parent = this; children.add(block); if (isAttached()) LytDocument.notifyAttach(block); + invalidateLayout(); } @Override @@ -63,10 +65,7 @@ public void replaceChild(LytNode oldChild, LytNode newChild) { newBlock.parent = this; children.set(idx, newBlock); if (isAttached()) LytDocument.notifyAttach(newBlock); - LytDocument doc = getDocument(); - if (doc != null) { - doc.invalidateLayout(); - } + invalidateLayout(); } public void clearContent() { @@ -74,6 +73,7 @@ public void clearContent() { child.parent = null; } children.clear(); + invalidateLayout(); } protected abstract LytRect computeBoxLayout(LayoutContext context, int x, int y, int availableWidth); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java index 9d6d2108..dadd58fe 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytCodeBlockToolbar.java @@ -100,7 +100,6 @@ public void addButton(LytButton button) { button.setColor(toolbarText); extraButtons.add(button); append(button); - if (getDocument() != null) getDocument().invalidateLayout(); } public void setPreferredWidth(int preferredWidth) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java index 2850fc23..05afdf48 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytContentTabsBlock.java @@ -260,9 +260,7 @@ public boolean mouseClicked(GuideUiHost screen, int x, int y, int button, boolea if (tabs.get(index).bounds.contains(x, y)) { if (selectedIndex != index) { selectedIndex = index; - if (getDocument() != null) { - getDocument().invalidateLayout(); - } + invalidateLayout(); } return true; } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java index 80b5efe1..a4936c24 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDetailsBlock.java @@ -111,10 +111,7 @@ public void setOpen(boolean open) { this.open = open; syncSummaryMarker(); syncContentVisibility(); - var document = getDocument(); - if (document != null) { - document.invalidateLayout(); - } + invalidateLayout(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java index 325f7e14..d918e3f7 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java @@ -146,6 +146,7 @@ static void notifyDetach(LytNode node) { cascadeLive(node, false); } + @Override public void invalidateLayout() { layout = null; invalidateVisibleCache(); diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java index d410e312..d861ee91 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocumentFloat.java @@ -61,8 +61,7 @@ public void replaceChild(LytNode oldChild, LytNode newChild) { inner.parent = null; inner = (LytBlock) newChild; inner.parent = this; - LytDocument doc = getDocument(); - if (doc != null) doc.invalidateLayout(); + invalidateLayout(); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java index 5691f71b..0ef31e20 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytNode.java @@ -33,6 +33,31 @@ public abstract class LytNode implements Styleable { @Nullable private String styleClass; + /** + * Receives layout changes for a detached layout tree, such as rich tooltip content. + */ + @Nullable + private Runnable detachedLayoutInvalidator; + + /** + * Invalidates the nearest layout owner. Attached nodes reach their document while detached + * trees can provide an explicit owner through {@link #setDetachedLayoutInvalidator(Runnable)}. + */ + public void invalidateLayout() { + if (parent != null) { + parent.invalidateLayout(); + } else if (detachedLayoutInvalidator != null) { + detachedLayoutInvalidator.run(); + } + } + + /** + * Sets the layout owner used when this node is the root of a detached layout tree. + */ + public void setDetachedLayoutInvalidator(@Nullable Runnable detachedLayoutInvalidator) { + this.detachedLayoutInvalidator = detachedLayoutInvalidator; + } + public void removeChild(LytNode node) {} public void replaceChild(LytNode oldChild, LytNode newChild) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java index 8ecc8174..34f740b0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytParagraph.java @@ -48,6 +48,7 @@ public class LytParagraph extends LytBlock implements LytFlowContainer, DebugFlo public void append(LytFlowContent child) { content.append(child); child.setParent(this); + invalidateLayout(); } @Override @@ -163,6 +164,7 @@ public boolean isEmpty() { public void clearContent() { content.clear(); + invalidateLayout(); } /** diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java index 8fc3e4fd..26d1a96b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytPlaceholderBlock.java @@ -32,13 +32,14 @@ public LytPlaceholderBlock(CompletableFuture future) { private void setCurrent(LytBlock block) { if (currentBlock != block) { + if (currentBlock != null) { + currentBlock.parent = null; + } currentChildren.clear(); currentBlock = block; + block.parent = this; currentChildren.add(block); - var document = getDocument(); - if (document != null) { - document.invalidateLayout(); - } + invalidateLayout(); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java index 01b0ec61..a4f27905 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowContent.java @@ -5,6 +5,7 @@ import org.jetbrains.annotations.Nullable; +import com.hfstudio.guidenh.guide.document.block.LytNode; import com.hfstudio.guidenh.guide.document.block.LytVisitor; import com.hfstudio.guidenh.guide.style.Styleable; import com.hfstudio.guidenh.guide.style.TextStyle; @@ -119,4 +120,15 @@ public void setData(String key, Object value) { data.put(key, value); } + /** + * Invalidates the block layout that owns this flow content, if it has one. + */ + public void invalidateLayout() { + if (parent instanceof LytFlowContent flowContent) { + flowContent.invalidateLayout(); + } else if (parent instanceof LytNode node) { + node.invalidateLayout(); + } + } + } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java index 12117251..b239c9b4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowInlineBlock.java @@ -18,7 +18,6 @@ import lombok.Setter; @Getter -@Setter public class LytFlowInlineBlock extends LytFlowContent implements InteractiveElement { private static final ThreadLocal MEASURE_LAYOUT_CONTEXT = ThreadLocal @@ -26,8 +25,20 @@ public class LytFlowInlineBlock extends LytFlowContent implements InteractiveEle private LytBlock block; + @Setter private InlineBlockAlignment alignment = InlineBlockAlignment.INLINE; + /** + * Replaces the rendered inline block and invalidates the owning paragraph layout. + */ + public void setBlock(@Nullable LytBlock block) { + if (this.block == block) { + return; + } + this.block = block; + invalidateLayout(); + } + public LytSize getPreferredSize(int lineWidth) { return measurePreferredBounds(lineWidth).size(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowSpan.java b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowSpan.java index e3f1865a..a89c6cc3 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowSpan.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/flow/LytFlowSpan.java @@ -22,6 +22,7 @@ public void append(LytFlowContent child) { } child.setParent(this); children.add(child); + invalidateLayout(); } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/interaction/ContentTooltip.java b/src/main/java/com/hfstudio/guidenh/guide/document/interaction/ContentTooltip.java index 6523d915..3d5acb72 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/interaction/ContentTooltip.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/interaction/ContentTooltip.java @@ -3,6 +3,7 @@ import com.hfstudio.guidenh.guide.document.LytRect; import com.hfstudio.guidenh.guide.document.block.LytBlock; import com.hfstudio.guidenh.guide.document.block.LytNode; +import com.hfstudio.guidenh.guide.document.block.LytVisitor; import com.hfstudio.guidenh.guide.layout.LayoutContext; import com.hfstudio.guidenh.guide.layout.MinecraftFontMetrics; import com.hfstudio.guidenh.guide.scene.LytGuidebookScene; @@ -19,16 +20,25 @@ public class ContentTooltip implements GuideTooltip { private int lastMaxWidth = -1; @Getter private LytRect layoutBox = LytRect.empty(); + /** + * The unnormalized bounds of all rendered tooltip content. This includes floating and inline + * blocks that can extend beyond their paragraph's text bounds. + */ + @Getter + private LytRect contentBounds = LytRect.empty(); public ContentTooltip(LytBlock content) { this.content = content; + content.setDetachedLayoutInvalidator(this::invalidateLayout); prepareEmbeddedScenes(content); } public LytRect layout(int maxWidth) { if (maxWidth != lastMaxWidth) { var ctx = new LayoutContext(new MinecraftFontMetrics()); - layoutBox = content.layout(ctx, 0, 0, Math.max(20, maxWidth)); + LytRect rootBounds = content.layout(ctx, 0, 0, Math.max(20, maxWidth)); + contentBounds = collectContentBounds(rootBounds); + layoutBox = new LytRect(0, 0, contentBounds.width(), contentBounds.height()); lastMaxWidth = maxWidth; } return layoutBox; @@ -40,6 +50,22 @@ public LytRect layout(int maxWidth) { public void invalidateLayout() { lastMaxWidth = -1; layoutBox = LytRect.empty(); + contentBounds = LytRect.empty(); + } + + private LytRect collectContentBounds(LytRect rootBounds) { + LytRect[] visualBounds = { rootBounds }; + content.visit(new LytVisitor() { + + @Override + public Result beforeNode(LytNode node) { + if (node instanceof LytBlock block && block.getBounds() != null) { + visualBounds[0] = LytRect.union(visualBounds[0], block.getBounds()); + } + return Result.CONTINUE; + } + }); + return visualBounds[0]; } @Override diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 451d4911..86805101 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -4427,6 +4427,7 @@ private void drawContentTooltip(ContentTooltip ct, int mouseX, int mouseY, LytRect bounds = resolveTooltipBounds(interaction); int maxW = Math.max(80, (bounds.width() * 4) / 5); var box = ct.layout(maxW); + LytRect contentBounds = ct.getContentBounds(); int w = box.width(); int h = box.height(); int x = mouseX + 12; @@ -4453,13 +4454,20 @@ private void drawContentTooltip(ContentTooltip ct, int mouseX, int mouseY, drawGradientRect(x + w + pad - 1, y - pad + 1, x + w + pad, y + h + pad - 1, ctBorderTop, ctBorderBottom); var ctx = reusableContentTooltipCtx; - cachedContentTooltipViewport = cachedRect(cachedContentTooltipViewport, 0, 0, w, h); + cachedContentTooltipViewport = cachedRect( + cachedContentTooltipViewport, + contentBounds.x(), + contentBounds.y(), + w, + h); ctx.setViewport(cachedContentTooltipViewport); ctx.setScreenHeight(this.height); - ctx.setDocumentOrigin(x, y); + int contentX = x - contentBounds.x(); + int contentY = y - contentBounds.y(); + ctx.setDocumentOrigin(contentX, contentY); ctx.setScrollOffsetY(0); GL11.glPushMatrix(); - GL11.glTranslatef(x, y, 300f); + GL11.glTranslatef(contentX, contentY, 300f); try { ct.getContent() .render(ctx); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java index 7c5e1370..1122c640 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/editor/SceneEditorScreen.java @@ -1735,6 +1735,7 @@ private void drawPreviewContentTooltip(ContentTooltip tooltip, int mouseX, int m int pad = 4; int maxWidth = Math.max(80, (this.width * 4) / 5); var box = tooltip.layout(maxWidth); + LytRect contentBounds = tooltip.getContentBounds(); int tooltipWidth = box.width(); int tooltipHeight = box.height(); int tooltipX = mouseX + 12; @@ -1821,13 +1822,16 @@ private void drawPreviewContentTooltip(ContentTooltip tooltip, int mouseX, int m tooltipY + tooltipHeight + pad - 1, borderTop, borderBottom); - previewTooltipRenderContext.setViewport(new LytRect(0, 0, tooltipWidth, tooltipHeight)); + previewTooltipRenderContext + .setViewport(new LytRect(contentBounds.x(), contentBounds.y(), tooltipWidth, tooltipHeight)); previewTooltipRenderContext.setScreenHeight(this.height); - previewTooltipRenderContext.setDocumentOrigin(tooltipX, tooltipY); + int contentX = tooltipX - contentBounds.x(); + int contentY = tooltipY - contentBounds.y(); + previewTooltipRenderContext.setDocumentOrigin(contentX, contentY); previewTooltipRenderContext.setScrollOffsetY(0); GL11.glPushMatrix(); - GL11.glTranslatef(tooltipX, tooltipY, 300f); + GL11.glTranslatef(contentX, contentY, 300f); try { tooltip.getContent() .render(previewTooltipRenderContext); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java index 30f3d5d2..99fb0403 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/host/ScriptContextImpl.java @@ -60,7 +60,6 @@ public void replace(Object newNode) { // if (node instanceof LytFlowInlineBlock wrapper && newNode instanceof LytBlock newBlock) { wrapper.setBlock(newBlock); - document.invalidateLayout(); recordResult(newBlock); return; } @@ -70,11 +69,7 @@ public void replace(Object newNode) { LytNode parent = ln.getParent(); if (parent != null) { parent.replaceChild(ln, newLn); - // Async block materialization can replace a root-document child - // (for example, a ScenePlaceholder). Not every block container - // owns the document-level invalidation contract, so make the - // replacement invalidate the active document here as well. - document.invalidateLayout(); + parent.invalidateLayout(); } recordResult(newLn); return; @@ -89,7 +84,7 @@ public void replace(Object newNode) { fc.setParent(null); newFc.setParent(span); children.set(idx, newFc); - document.invalidateLayout(); + newFc.invalidateLayout(); } recordResult(newFc); return; @@ -103,7 +98,7 @@ public void replace(Object newNode) { fc.setParent(null); newFc.setParent(para); list.set(idx, newFc); - document.invalidateLayout(); + newFc.invalidateLayout(); } } recordResult(newFc); diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java index 7837dabe..441a12f0 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java @@ -704,6 +704,7 @@ private void drawContentTooltip(ContentTooltip tooltip, int mouseX, int mouseY) int right = width - padding; int bottom = height - padding; LytRect box = tooltip.layout(Math.max(80, (right - left) * 4 / 5)); + LytRect contentBounds = tooltip.getContentBounds(); int tooltipWidth = box.width(); int tooltipHeight = box.height(); int x = mouseX + 12; @@ -772,13 +773,16 @@ private void drawContentTooltip(ContentTooltip tooltip, int mouseX, int mouseY) borderTop, borderBottom); - contentTooltipRenderContext.setViewport(new LytRect(0, 0, tooltipWidth, tooltipHeight)); + contentTooltipRenderContext + .setViewport(new LytRect(contentBounds.x(), contentBounds.y(), tooltipWidth, tooltipHeight)); contentTooltipRenderContext.setScreenHeight(height); - contentTooltipRenderContext.setDocumentOrigin(x, y); + int contentX = x - contentBounds.x(); + int contentY = y - contentBounds.y(); + contentTooltipRenderContext.setDocumentOrigin(contentX, contentY); contentTooltipRenderContext.setScrollOffsetY(0); contentTooltipRenderContext.setZoom(1F); GL11.glPushMatrix(); - GL11.glTranslatef(x, y, 300F); + GL11.glTranslatef(contentX, contentY, 300F); try { tooltip.getContent() .render(contentTooltipRenderContext); diff --git a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialGeneratedBlock.java b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialGeneratedBlock.java index b5cddb0a..d5183c85 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialGeneratedBlock.java +++ b/src/main/java/com/hfstudio/guidenh/guide/mediawiki/MediaWikiSpecialGeneratedBlock.java @@ -149,10 +149,7 @@ public void setSearchQuery(String searchQuery) { currentVisibleCount = resolveDefaultVisibleCount(result); } visibilityCache = null; - var document = getDocument(); - if (document != null) { - document.invalidateLayout(); - } + invalidateLayout(); } private boolean supportsResolverBackedSearch() { @@ -674,10 +671,7 @@ private void loadMore() { Integer.MAX_VALUE - MediaWikiSpecialPageQuery.PAGE_SIZE, currentVisibleCount + MediaWikiSpecialPageQuery.PAGE_SIZE); visibilityCache = null; - var document = getDocument(); - if (document != null) { - document.invalidateLayout(); - } + invalidateLayout(); } private int estimateHeight(GroupLayout group) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java index 25cc7b17..3c32710d 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java +++ b/src/main/java/com/hfstudio/guidenh/guide/scene/LytGuidebookScene.java @@ -954,10 +954,7 @@ public BlockStatsLayoutState getBlockStatsLayoutStateForExport() { } private void invalidateDocumentLayout() { - var document = getDocument(); - if (document != null) { - document.invalidateLayout(); - } + invalidateLayout(); } public boolean hasVisibleLayerData() { From b8c8181bcc08153cf56e3ad23c0bbf510f204016 Mon Sep 17 00:00:00 2001 From: ABKQPO <93412322+ABKQPO@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:15:11 +0800 Subject: [PATCH 2/2] refresh when the version changes --- .../com/hfstudio/guidenh/guide/GuidePage.java | 35 ++++++++++++++++--- .../guide/document/block/LytDocument.java | 8 +++++ .../guidenh/guide/internal/GuideScreen.java | 29 +++++---------- .../internal/welcome/GuideWelcomeScreen.java | 19 +--------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/hfstudio/guidenh/guide/GuidePage.java b/src/main/java/com/hfstudio/guidenh/guide/GuidePage.java index 56a40087..ab56379b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/GuidePage.java +++ b/src/main/java/com/hfstudio/guidenh/guide/GuidePage.java @@ -2,7 +2,10 @@ import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Set; import net.minecraft.util.ResourceLocation; @@ -21,6 +24,8 @@ public class GuidePage { private final ResourceLocation id; private final LytDocument document; private final List scenes; + private final Set registeredScenes; + private long registeredScenesContentRevision; @Nullable private final LytHeading titleHeading; @Nullable @@ -42,6 +47,9 @@ public GuidePage(String sourcePack, ResourceLocation id, LytDocument document, @ this.titleHeading = titleHeading; this.pageMeta = pageMeta; this.scenes = collectScenes(document); + this.registeredScenes = Collections.newSetFromMap(new IdentityHashMap()); + this.registeredScenes.addAll(scenes); + this.registeredScenesContentRevision = document.getContentRevision(); } public String sourcePack() { @@ -84,7 +92,7 @@ public void prepareForDisplay() { public void releaseRuntimeScenes() { // MaterializeTask may append GameScene nodes after compilation. Discover those nodes before // releasing so asynchronously created preview worlds cannot outlive this page. - registerMaterializedScenes(); + registerMaterializedScenes(true); for (LytGuidebookScene scene : scenes) { if (scene != null) { GuidebookLevel level = scene.getLevel(); @@ -95,23 +103,40 @@ public void releaseRuntimeScenes() { } } - /** Adds scenes materialized into the document after the initial page compilation. */ - public void registerMaterializedScenes() { + /** + * Adds scenes materialized into the document after the initial page compilation when its + * content changed since the previous scan. + * + * @return the number of newly registered scenes + */ + public int refreshMaterializedScenes() { + return registerMaterializedScenes(false); + } + + private int registerMaterializedScenes(boolean force) { if (document == null) { - return; + return 0; + } + long contentRevision = document.getContentRevision(); + if (!force && contentRevision == registeredScenesContentRevision) { + return 0; } ArrayDeque pending = new ArrayDeque<>(); pending.add(document); + int added = 0; while (!pending.isEmpty()) { LytNode node = pending.removeLast(); - if (node instanceof LytGuidebookScene scene && !scenes.contains(scene)) { + if (node instanceof LytGuidebookScene scene && registeredScenes.add(scene)) { scenes.add(scene); + added++; } List children = node.getChildren(); for (int i = children.size() - 1; i >= 0; i--) { pending.addLast(children.get(i)); } } + registeredScenesContentRevision = contentRevision; + return added; } private static List collectScenes(LytDocument document) { diff --git a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java index d918e3f7..24326ec8 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java +++ b/src/main/java/com/hfstudio/guidenh/guide/document/block/LytDocument.java @@ -34,6 +34,13 @@ public class LytDocument extends LytNode implements LytBlockContainer { @Nullable private Layout layout; + /** + * Advances whenever document content invalidates its layout. Consumers can use this to refresh + * derived indexes without rescanning an unchanged document tree every client tick. + */ + @Getter + private long contentRevision; + @Nullable private DocumentInteractionSnapshot hoveredElement; @@ -148,6 +155,7 @@ static void notifyDetach(LytNode node) { @Override public void invalidateLayout() { + contentRevision++; layout = null; invalidateVisibleCache(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java index 86805101..8f85957b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -2534,8 +2534,8 @@ private void tickCurrentPageScenes() { if (!pendingSceneRegistrations.isEmpty()) { return; } - // Phase 3: Scenes created asynchronously (MaterializeTask) appear in the document - // tree after mountDocument returns. Scan for new scenes each tick. + // Scenes created by MaterializeTask appear after page compilation. GuidePage only scans + // when the document content revision has changed. registerRuntimeScenes(currentPage); for (LytGuidebookScene scene : currentPage.scenes()) { scene.ponderTick(); @@ -2622,26 +2622,13 @@ private void completePendingContentPageLoadIfNeeded() { /** Register scenes created at MOUNT time into GuidePage.scenes() for tick dispatch. */ private static void registerRuntimeScenes(GuidePage page) { - LytDocument doc = page.document(); - if (doc == null) return; - List list = page.scenes(); - ArrayDeque pending = new ArrayDeque<>(); - pending.add(doc); - int found = 0; - while (!pending.isEmpty()) { - LytNode node = pending.removeLast(); - if (node instanceof LytGuidebookScene scene && !list.contains(scene)) { - list.add(scene); - found++; - } - var children = node.getChildren(); - for (int i = children.size() - 1; i >= 0; i--) { - pending.addLast(children.get(i)); - } - } + int found = page.refreshMaterializedScenes(); if (found > 0) { - GuideDebugLog - .info("[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", found, list.size()); + GuideDebugLog.info( + "[PonderDebug] registerRuntimeScenes: registered {} new scenes, total={}", + found, + page.scenes() + .size()); } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java b/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java index 441a12f0..4d073e52 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/welcome/GuideWelcomeScreen.java @@ -2,7 +2,6 @@ import java.awt.Desktop; import java.net.URI; -import java.util.ArrayDeque; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -370,23 +369,7 @@ private void tickScenes() { } private static void registerRuntimeScenes(GuidePage page) { - LytDocument doc = page.document(); - if (doc == null) { - return; - } - List scenes = page.scenes(); - ArrayDeque pending = new ArrayDeque<>(); - pending.add(doc); - while (!pending.isEmpty()) { - LytNode node = pending.removeLast(); - if (node instanceof LytGuidebookScene scene && !scenes.contains(scene)) { - scenes.add(scene); - } - List children = node.getChildren(); - for (int i = children.size() - 1; i >= 0; i--) { - pending.addLast(children.get(i)); - } - } + page.refreshMaterializedScenes(); } @Override