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 750f7d8d..1228e6af 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/GuideScreen.java @@ -10,7 +10,6 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.Deque; import java.util.LinkedHashMap; @@ -783,7 +782,9 @@ private void expandNavigationParentsToCurrentPage() { if (!hasContentRoute()) { return; } - navBar.expandParentsTo(resolveNavigationTree(), currentAnchor.pageId(), bookmarkState); + ResourceLocation currentGuideId = currentRoute != null && currentRoute.isContent() ? currentRoute.guideId() + : guide != null ? guide.getId() : null; + navBar.expandParentsTo(resolveNavigationTree(), currentGuideId, currentAnchor.pageId(), bookmarkState); } private void applyPendingRestoreScroll() { @@ -818,9 +819,9 @@ private void rememberNavigationState() { private void updateSavedExpansionStates(List changes, @Nullable ResourceLocation currentGuideId) { Map savedStates = new LinkedHashMap<>(); - Map> expandedPageIdsByGuide = new LinkedHashMap<>(); + Map> expandedNodeKeysByGuide = new LinkedHashMap<>(); for (ExpansionChange change : changes) { - ResourceLocation guideId = change.guideId(); + ResourceLocation guideId = change.guideId() != null ? change.guideId() : currentGuideId; if (guideId == null && currentGuideId == null) { continue; } @@ -829,14 +830,14 @@ private void updateSavedExpansionStates(List changes, @Nullable ignored -> ClientProxy.getLytHost() .getNavigation() .recallNavigationState(guideId)); - LinkedHashSet expandedPageIds = expandedPageIdsByGuide.computeIfAbsent( + LinkedHashSet expandedNodeKeys = expandedNodeKeysByGuide.computeIfAbsent( guideId, - ignored -> new LinkedHashSet<>( - saved.expandedPageIds() != null ? saved.expandedPageIds() : Collections.emptySet())); + ignored -> new LinkedHashSet<>(saved.expandedNodeKeys() != null ? saved.expandedNodeKeys() : Set.of())); + NavigationNode.Key key = new NavigationNode.Key(guideId, change.pageId()); if (change.expanded()) { - expandedPageIds.add(change.pageId()); + expandedNodeKeys.add(key); } else { - expandedPageIds.remove(change.pageId()); + expandedNodeKeys.remove(key); } } for (Map.Entry entry : savedStates.entrySet()) { @@ -845,9 +846,9 @@ private void updateSavedExpansionStates(List changes, @Nullable .getNavigation() .rememberNavBarState( entry.getKey(), - GuideNavBarState.create( + GuideNavBarState.createExpandedNodes( saved.bookmarkGroupExpanded(), - expandedPageIdsByGuide.get(entry.getKey()), + expandedNodeKeysByGuide.get(entry.getKey()), saved.scrollY())); } } @@ -2547,7 +2548,7 @@ private void navigateBackInHistory() { .guideId() : null; ResourceLocation oldGuideId = guide != null ? guide.getId() : null; boolean guideChanged = !Objects.equals(oldGuideId, prevGuideId); - Set carryOver = guideChanged ? navBar.getExpandedPageIdsSnapshot() : null; + Set carryOver = guideChanged ? navBar.getExpandedNodeKeysSnapshot() : null; restoreViewState(prev); if (guideChanged) { navBar.activateGuide( @@ -2584,7 +2585,7 @@ private void navigateForwardInHistory() { .guideId() : null; ResourceLocation oldGuideId = guide != null ? guide.getId() : null; boolean guideChanged = !Objects.equals(oldGuideId, nextGuideId); - Set carryOver = guideChanged ? navBar.getExpandedPageIdsSnapshot() : null; + Set carryOver = guideChanged ? navBar.getExpandedNodeKeysSnapshot() : null; restoreViewState(next); if (guideChanged) { navBar.activateGuide( @@ -6843,7 +6844,7 @@ public void navigateTo(ResourceLocation guideId, PageAnchor anchor) { suppressGuideEditorTextFocusUntilGuideHotkeyRelease(); rememberCurrentContentStateIfEligible(); rememberNavigationState(); - Set carryOver = navBar.getExpandedPageIdsSnapshot(); + Set carryOver = navBar.getExpandedNodeKeysSnapshot(); restoreViewState(GuideScreenViewState.of(GuideScreenRoute.content(guideId, anchor), 0)); navBar.activateGuide( guideId, diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java index ba3a422a..8d6d9ba4 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBar.java @@ -3,7 +3,6 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -168,7 +167,7 @@ public interface GuideExpansionListener { public record ExpansionChange(@Nullable ResourceLocation guideId, ResourceLocation pageId, boolean expanded) {} private final List rows = new ArrayList<>(); - private final Set expandedPageIds = new HashSet<>(); + private final Set expandedNodeKeys = new HashSet<>(); private final GuideNavProjection projection = new GuideNavProjection(); private final StickyStack stickyStack = new StickyStack(); @Nullable @@ -270,16 +269,16 @@ public void update(int mouseX, int mouseY, @Nullable NavigationTree tree, GuideB } public GuideNavBarState captureState() { - return GuideNavBarState.create(bookmarkGroupExpanded, new LinkedHashSet<>(expandedPageIds), scrollY); + return GuideNavBarState + .createExpandedNodes(bookmarkGroupExpanded, new LinkedHashSet<>(expandedNodeKeys), scrollY); } public void restoreState(GuideNavBarState state, GuideBookmarkState bookmarkState) { releaseScrollbar(0); GuideNavBarState effectiveState = state != null ? state : GuideNavBarState.defaultState(); bookmarkGroupExpanded = effectiveState.bookmarkGroupExpanded(); - expandedPageIds.clear(); - expandedPageIds.addAll( - effectiveState.expandedPageIds() != null ? effectiveState.expandedPageIds() : Collections.emptySet()); + expandedNodeKeys.clear(); + expandedNodeKeys.addAll(resolveExpansionKeys(effectiveState.expandedNodeKeys(), lastTree)); expandedStateVersion++; scrollY = effectiveState.scrollY(); visualScrollY.snapTo(scrollY); @@ -290,25 +289,24 @@ public void restoreState(GuideNavBarState state, GuideBookmarkState bookmarkStat /** * Switch the nav bar to a different guide's expansion state. - * Replaces {@code expandedPageIds} with the saved state for the new guide, + * Replaces the expanded node keys with the saved state for the new guide, * then expands ancestors of {@code currentPageId}. * Caller is responsible for saving the old guide's state before calling this. */ public void activateGuide(@Nullable ResourceLocation guideId, GuideNavBarState savedState, @Nullable NavigationTree tree, GuideBookmarkState bookmarkState, @Nullable ResourceLocation currentPageId, - Set carryOverIds) { + Set carryOverKeys) { // 1. Replace with new guide's saved state - expandedPageIds.clear(); - if (savedState.expandedPageIds() != null) { - expandedPageIds.addAll(savedState.expandedPageIds()); - } - - // 2. Merge carry-over IDs that exist in the new tree (captured before restoreViewState) - if (carryOverIds != null && tree != null) { - for (ResourceLocation id : carryOverIds) { - if (tree.getNodeById(id) != null) { - expandedPageIds.add(id); + expandedNodeKeys.clear(); + expandedNodeKeys.addAll(resolveExpansionKeys(savedState.expandedNodeKeys(), tree)); + + // 2. Merge carry-over expansions that exist in the new tree. + if (carryOverKeys != null && tree != null) { + for (NavigationNode.Key key : carryOverKeys) { + NavigationNode node = tree.getNodeById(guideId, key.pageId()); + if (node != null && node.key() != null) { + expandedNodeKeys.add(node.key()); } } } @@ -324,12 +322,12 @@ public void activateGuide(@Nullable ResourceLocation guideId, GuideNavBarState s // 3. Expand ancestors of current page (inline, single rebuild at end) if (currentPageId != null && tree != null) { - var path = tree.getPathTo(currentPageId); + var path = tree.getPathTo(guideId, currentPageId); for (int i = 0; i < path.size() - 1; i++) { - ResourceLocation parentId = path.get(i) - .pageId(); - if (parentId != null) { - expandedPageIds.add(parentId); + NavigationNode.Key parentKey = path.get(i) + .key(); + if (parentKey != null) { + expandedNodeKeys.add(parentKey); } } } @@ -341,24 +339,42 @@ public void activateGuide(@Nullable ResourceLocation guideId, GuideNavBarState s } } - /** Snapshot of current expanded page IDs for carry-over before navigation. */ - public Set getExpandedPageIdsSnapshot() { - return new HashSet<>(expandedPageIds); + /** Snapshot of current expanded node keys for carry-over before navigation. */ + public Set getExpandedNodeKeysSnapshot() { + return new HashSet<>(expandedNodeKeys); } - public void expandParentsTo(@Nullable NavigationTree tree, @Nullable ResourceLocation pageId, - GuideBookmarkState bookmarkState) { + private Set resolveExpansionKeys(Set keys, @Nullable NavigationTree tree) { + if (keys == null || keys.isEmpty() || tree == null) { + return keys == null ? Set.of() : new HashSet<>(keys); + } + Set resolved = new HashSet<>(); + for (NavigationNode.Key key : keys) { + if (key == null) { + continue; + } + NavigationNode node = key.guideId() == null ? tree.getNodeById(key.pageId()) + : tree.getNodeById(key.guideId(), key.pageId()); + if (node != null && node.key() != null) { + resolved.add(node.key()); + } + } + return resolved; + } + + public void expandParentsTo(@Nullable NavigationTree tree, @Nullable ResourceLocation guideId, + @Nullable ResourceLocation pageId, GuideBookmarkState bookmarkState) { if (tree == null || pageId == null) { return; } - var path = tree.getPathTo(pageId); + var path = tree.getPathTo(guideId, pageId); boolean changed = false; for (int index = 0; index < path.size() - 1; index++) { - ResourceLocation parentPageId = path.get(index) - .pageId(); - if (parentPageId != null) { - changed |= expandedPageIds.add(parentPageId); + NavigationNode.Key parentKey = path.get(index) + .key(); + if (parentKey != null) { + changed |= expandedNodeKeys.add(parentKey); } } if (changed) { @@ -404,7 +420,7 @@ private void rebuildRows(@Nullable NavigationTree tree, GuideBookmarkState bookm return; } GuideNavProjection.ProjectionResult projected = projection - .project(tree, bookmarkState, expandedPageIds, bookmarkGroupExpanded); + .project(tree, bookmarkState, expandedNodeKeys, bookmarkGroupExpanded); projected = projection.withTemplates(projected, templateRows, templateGroupExpanded); for (GuideNavProjection.ProjectedRow projectedRow : projected.rows()) { rows.add(new Row(projectedRow)); @@ -900,7 +916,8 @@ private boolean isCollapsed(Row row) { } private boolean isExpanded(Row row) { - return row.pageId() != null && expandedPageIds.contains(row.pageId()); + NavigationNode.Key key = row.navigationKey(); + return key != null && expandedNodeKeys.contains(key); } private void toggleExpand(Row row, GuideBookmarkState bookmarkState) { @@ -918,11 +935,12 @@ private void toggleExpand(Row row, GuideBookmarkState bookmarkState) { if (lastTree == null || pageId == null) { return; } - NavigationNode node = lastTree.getNodeById(pageId); + NavigationNode node = lastTree.getNodeById(row.guideId(), pageId); if (node == null) { return; } - updateExpansionState(List.of(node), !expandedPageIds.contains(pageId), bookmarkState); + NavigationNode.Key key = node.key(); + updateExpansionState(List.of(node), key == null || !expandedNodeKeys.contains(key), bookmarkState); } private void toggleExpandedDescendants(Row row, GuideBookmarkState bookmarkState) { @@ -930,13 +948,16 @@ private void toggleExpandedDescendants(Row row, GuideBookmarkState bookmarkState if (lastTree == null || pageId == null) { return; } - NavigationNode node = lastTree.getNodeById(pageId); + NavigationNode node = lastTree.getNodeById(row.guideId(), pageId); if (node == null) { return; } List nodes = collectExpandableNodes(node); boolean allExpanded = !nodes.isEmpty() && nodes.stream() - .allMatch(expandableNode -> expandedPageIds.contains(expandableNode.pageId())); + .allMatch(expandableNode -> { + NavigationNode.Key key = expandableNode.key(); + return key != null && expandedNodeKeys.contains(key); + }); updateExpansionState(nodes, !allExpanded, bookmarkState); } @@ -977,20 +998,20 @@ private void updateExpansionState(List nodes, boolean expanded, private void updateExpansionState(List nodes, boolean expanded, GuideBookmarkState bookmarkState, boolean allCollapsed) { List changes = new ArrayList<>(); - boolean changed = allCollapsed && !expandedPageIds.isEmpty(); + boolean changed = allCollapsed && !expandedNodeKeys.isEmpty(); for (NavigationNode node : nodes) { - ResourceLocation pageId = node.pageId(); - if (pageId == null) { + NavigationNode.Key key = node.key(); + if (key == null) { continue; } - boolean nodeChanged = expanded ? expandedPageIds.add(pageId) : expandedPageIds.remove(pageId); + boolean nodeChanged = expanded ? expandedNodeKeys.add(key) : expandedNodeKeys.remove(key); changed |= nodeChanged; if (nodeChanged) { - changes.add(new ExpansionChange(node.guideId(), pageId, expanded)); + changes.add(new ExpansionChange(key.guideId(), key.pageId(), expanded)); } } if (allCollapsed) { - expandedPageIds.clear(); + expandedNodeKeys.clear(); } if (!changed) { return; @@ -1378,6 +1399,11 @@ public ResourceLocation pageId() { return pageId; } + @Nullable + public NavigationNode.Key navigationKey() { + return pageId != null ? new NavigationNode.Key(guideId, pageId) : null; + } + public GuideNavProjection.RowKind kind() { return displayRow.kind(); } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBarState.java b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBarState.java index c7b43dbf..4b97dc00 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBarState.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavBarState.java @@ -1,23 +1,31 @@ package com.hfstudio.guidenh.guide.internal.screen; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; import net.minecraft.util.ResourceLocation; +import com.hfstudio.guidenh.guide.navigation.NavigationNode; + public class GuideNavBarState { - private static final GuideNavBarState DEFAULT_STATE = new GuideNavBarState(true, Set.of(), 0); + private static final GuideNavBarState DEFAULT_STATE = createExpandedNodes(true, Set.of(), 0); private final boolean bookmarkGroupExpanded; - private final Set expandedPageIds; + private final Set expandedNodeKeys; private final int scrollY; public GuideNavBarState(boolean bookmarkGroupExpanded, Set expandedPageIds, int scrollY) { + this(bookmarkGroupExpanded, fromPageIds(expandedPageIds), scrollY); + } + + private GuideNavBarState(boolean bookmarkGroupExpanded, Collection expandedNodeKeys, + int scrollY) { this.bookmarkGroupExpanded = bookmarkGroupExpanded; - this.expandedPageIds = Set - .copyOf(expandedPageIds == null ? new LinkedHashSet<>() : new LinkedHashSet<>(expandedPageIds)); + this.expandedNodeKeys = Set + .copyOf(expandedNodeKeys == null ? new LinkedHashSet<>() : new LinkedHashSet<>(expandedNodeKeys)); this.scrollY = Math.max(0, scrollY); } @@ -26,6 +34,11 @@ public static GuideNavBarState create(boolean bookmarkGroupExpanded, Set expandedNodeKeys, int scrollY) { + return new GuideNavBarState(bookmarkGroupExpanded, expandedNodeKeys, scrollY); + } + public static GuideNavBarState defaultState() { return DEFAULT_STATE; } @@ -35,7 +48,15 @@ public boolean bookmarkGroupExpanded() { } public Set expandedPageIds() { - return expandedPageIds; + Set pageIds = new LinkedHashSet<>(); + for (NavigationNode.Key key : expandedNodeKeys) { + pageIds.add(key.pageId()); + } + return Set.copyOf(pageIds); + } + + public Set expandedNodeKeys() { + return expandedNodeKeys; } public int scrollY() { @@ -50,12 +71,24 @@ public boolean equals(Object obj) { if (!(obj instanceof GuideNavBarState other)) { return false; } - return bookmarkGroupExpanded == other.bookmarkGroupExpanded && expandedPageIds.equals(other.expandedPageIds) + return bookmarkGroupExpanded == other.bookmarkGroupExpanded && expandedNodeKeys.equals(other.expandedNodeKeys) && scrollY == other.scrollY; } @Override public int hashCode() { - return Objects.hash(bookmarkGroupExpanded, expandedPageIds, scrollY); + return Objects.hash(bookmarkGroupExpanded, expandedNodeKeys, scrollY); + } + + private static Set fromPageIds(Set pageIds) { + Set keys = new LinkedHashSet<>(); + if (pageIds != null) { + for (ResourceLocation pageId : pageIds) { + if (pageId != null) { + keys.add(new NavigationNode.Key(null, pageId)); + } + } + } + return keys; } } diff --git a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavProjection.java b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavProjection.java index d00c733e..58d58dcb 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavProjection.java +++ b/src/main/java/com/hfstudio/guidenh/guide/internal/screen/GuideNavProjection.java @@ -145,7 +145,7 @@ public boolean hasPage() { .toString() : ""); public ProjectionResult project(@Nullable NavigationTree tree, GuideBookmarkState bookmarkState, - Set expandedTreePageIds, boolean bookmarkGroupExpanded) { + Set expandedTreeNodeKeys, boolean bookmarkGroupExpanded) { if (tree == null) { return new ProjectionResult(List.of()); } @@ -179,7 +179,7 @@ public ProjectionResult project(@Nullable NavigationTree tree, GuideBookmarkStat } for (var root : tree.getRootNodes()) { - addTreeRows(rows, root, 0, expandedTreePageIds, -1); + addTreeRows(rows, root, 0, expandedTreeNodeKeys, -1); } return new ProjectionResult(rows); } @@ -260,7 +260,7 @@ private ProjectedRow appendRow(List rows, DisplayRow displayRow, i } private void addTreeRows(List rows, NavigationNode node, int depth, - Set expandedTreePageIds, int parentRowIndex) { + Set expandedTreeNodeKeys, int parentRowIndex) { ProjectedRow row = appendRow( rows, new DisplayRow( @@ -274,9 +274,10 @@ private void addTreeRows(List rows, NavigationNode node, int depth .isEmpty(), node.hasPage()), parentRowIndex); - if (node.pageId() != null && expandedTreePageIds.contains(node.pageId())) { + NavigationNode.Key nodeKey = node.key(); + if (nodeKey != null && expandedTreeNodeKeys.contains(nodeKey)) { for (var child : node.children()) { - addTreeRows(rows, child, depth + 1, expandedTreePageIds, row.rowIndex()); + addTreeRows(rows, child, depth + 1, expandedTreeNodeKeys, row.rowIndex()); } } row.setSubtreeEndRowIndexExclusive(rows.size()); diff --git a/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationNode.java b/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationNode.java index ffd97458..c476a424 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationNode.java +++ b/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationNode.java @@ -9,4 +9,12 @@ import com.hfstudio.guidenh.guide.GuidePageIcon; public record NavigationNode(@Nullable ResourceLocation guideId, @Nullable ResourceLocation pageId, String title, - @Nullable GuidePageIcon icon, List children, int position, boolean hasPage) {} + @Nullable GuidePageIcon icon, List children, int position, boolean hasPage) { + + public record Key(@Nullable ResourceLocation guideId, ResourceLocation pageId) {} + + @Nullable + public Key key() { + return pageId != null ? new Key(guideId, pageId) : null; + } +} diff --git a/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java b/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java index 57dbf5f2..6669219b 100644 --- a/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java +++ b/src/main/java/com/hfstudio/guidenh/guide/navigation/NavigationTree.java @@ -52,6 +52,15 @@ public NavigationNode getNodeById(ResourceLocation pageId) { return nodeIndex.get(pageId); } + @Nullable + public NavigationNode getNodeById(@Nullable ResourceLocation guideId, ResourceLocation pageId) { + NavigationNode indexed = nodeIndex.get(pageId); + if (indexed != null && Objects.equals(indexed.guideId(), guideId)) { + return indexed; + } + return findNode(rootNodes, guideId, pageId); + } + @Nullable public ResourceLocation getParentId(ResourceLocation pageId) { return parentIndex.get(pageId); @@ -72,6 +81,41 @@ public List getPathTo(ResourceLocation pageId) { return path; } + public List getPathTo(@Nullable ResourceLocation guideId, ResourceLocation pageId) { + List path = new ArrayList<>(); + return findPath(rootNodes, guideId, pageId, path) ? List.copyOf(path) : List.of(); + } + + @Nullable + private static NavigationNode findNode(List nodes, @Nullable ResourceLocation guideId, + ResourceLocation pageId) { + for (NavigationNode node : nodes) { + if (Objects.equals(node.guideId(), guideId) && Objects.equals(node.pageId(), pageId)) { + return node; + } + NavigationNode childMatch = findNode(node.children(), guideId, pageId); + if (childMatch != null) { + return childMatch; + } + } + return null; + } + + private static boolean findPath(List nodes, @Nullable ResourceLocation guideId, + ResourceLocation pageId, List path) { + for (NavigationNode node : nodes) { + path.add(node); + if (Objects.equals(node.guideId(), guideId) && Objects.equals(node.pageId(), pageId)) { + return true; + } + if (findPath(node.children(), guideId, pageId, path)) { + return true; + } + path.remove(path.size() - 1); + } + return false; + } + public static NavigationTree build(Collection pages) { return build(null, pages); } diff --git a/src/main/java/com/hfstudio/guidenh/libs/micromark/commonmark/Attention.java b/src/main/java/com/hfstudio/guidenh/libs/micromark/commonmark/Attention.java index 5c0c0c28..3a733e95 100644 --- a/src/main/java/com/hfstudio/guidenh/libs/micromark/commonmark/Attention.java +++ b/src/main/java/com/hfstudio/guidenh/libs/micromark/commonmark/Attention.java @@ -255,7 +255,8 @@ State sequence(int code) { var open = after == 0 || (after == Constants.characterGroupPunctuation && before != 0) || attentionMarkers.contains(code); - var close = before == 0 || (before == Constants.characterGroupPunctuation && after != 0) + var close = before == 0 + || (before == Constants.characterGroupPunctuation && after != Constants.characterGroupWhitespace) || attentionMarkers.contains(previous); token._open = marker == Codes.asterisk ? open : open && (before != 0 || !close);