From a68a55205ed189250693368af7028031a70631d9 Mon Sep 17 00:00:00 2001
From: Terry Truong <terry06890@gmail.com>
Date: Tue, 29 Mar 2022 11:39:33 +1100
Subject: Update comments, adjust names, do minor refactors

---
 src/App.vue | 436 +++++++++++++++++++++++++++---------------------------------
 1 file changed, 194 insertions(+), 242 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index b28531a..cf25b18 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,114 +1,114 @@
 <script lang="ts">
 import {defineComponent, PropType} from 'vue';
-//
+// Components
 import Tile from './components/Tile.vue';
 import AncestryBar from './components/AncestryBar.vue';
 import TileInfoModal from './components/TileInfoModal.vue';
-import SearchModal from './components/SearchModal.vue';
 import HelpModal from './components/HelpModal.vue';
+import SearchModal from './components/SearchModal.vue';
 import SettingsPane from './components/SettingsPane.vue';
-//
+// Icons
+import HelpIcon from './components/icon/HelpIcon.vue';
 import SearchIcon from './components/icon/SearchIcon.vue';
 import PlayIcon from './components/icon/PlayIcon.vue';
-import HelpIcon from './components/icon/HelpIcon.vue';
 import SettingsIcon from './components/icon/SettingsIcon.vue';
-//
+// Other
 import {TolNode, TolNodeRaw, tolFromRaw, getTolMap} from './tol';
 import {LayoutNode, initLayoutTree, initLayoutMap, tryLayout} from './layout';
 import type {LayoutOptions} from './layout';
 import {arraySum, randWeightedChoice} from './util';
-// Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain
+// Note: Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain
+
+// Type representing auto-mode actions
+type Action = 'move across' | 'move down' | 'move up' |
+	'expand' | 'collapse' | 'expand to view' | 'expand ancestry bar';
+// Used in auto-mode to help avoid action cycles
+function getReverseAction(action: Action): Action | null {
+	let reversePairs: Action[][] = [
+		['move down', 'move up'],
+		['expand', 'collapse'],
+		['expand to view', 'expand ancestry bar'],
+	];
+	let pair = reversePairs.find(pair => pair.includes(action));
+	if (pair != null){
+		return pair[0] == action ? pair[1] : pair[0];
+	} else {
+		return null;
+	}
+}
 
-// Obtain tree-of-life data
+// Get tree-of-life data
 import tolRaw from './tolData.json';
-const tol: TolNode = tolFromRaw(tolRaw);
+const tol = tolFromRaw(tolRaw);
 const tolMap = getTolMap(tol);
 
-// Configurable settings
+// Configurable options
 const defaultLytOpts: LayoutOptions = {
 	tileSpacing: 8, //px
-	headerSz: 20, //px
+	headerSz: 22, //px
 	minTileSz: 50, //px
 	maxTileSz: 200, //px
+	// Layout-algorithm related
 	layoutType: 'sweep', //'sqr' | 'rect' | 'sweep'
 	rectMode: 'auto first-row', //'horz' | 'vert' | 'linear' | 'auto' | 'auto first-row'
 	sweepMode: 'left', //'left' | 'top' | 'shorter' | 'auto'
 	sweptNodesPrio: 'pow-2/3', //'linear' | 'sqrt' | 'pow-2/3'
-	sweepingToParent: true,
+	sweepToParent: true,
 };
 const defaultUiOpts = {
-	// For leaf/non_leaf tile and detached-ancestor components
+	// For tiles
 	borderRadius: 5, //px
 	shadowNormal: '0 0 2px black',
 	shadowHighlight: '0 0 1px 2px greenyellow',
 	shadowFocused: '0 0 1px 2px orange',
-	// For leaf and detached-ancestor components
-	imgTilePadding: 4, //px
-	imgTileFontSz: 15, //px
-	imgTileColor: '#fafaf9',
-	expandableImgTileColor: 'greenyellow', //yellow, greenyellow, turquoise,
-	// For non-leaf tile-group components
-	nonLeafBgColors: ['#44403c', '#57534e'], //tiles at depth N use the Nth color, repeating from the start as needed
-	nonLeafHeaderFontSz: 15, //px
-	nonLeafHeaderColor: '#fafaf9',
-	nonLeafHeaderBgColor: '#1c1917',
-	// For tile-info modal
+	infoIconSz: 18, //px
+	infoIconMargin: 2, //px
+	// For leaf tiles
+	leafTilePadding: 4, //px
+	leafHeaderFontSz: 15, //px
+	leafHeaderColor: '#fafaf9',
+	leafHeaderExColor: 'greenyellow', //yellow, greenyellow, turquoise,
+	// For non-leaf tiles
+	nonleafBgColors: ['#44403c', '#57534e'], //tiles at depth N use the Nth color, repeating from the start as needed
+	nonleafHeaderFontSz: 15, //px
+	nonleafHeaderColor: '#fafaf9',
+	nonleafHeaderBgColor: '#1c1917',
+	// For other components
+	appBgColor: '#292524',
+	tileAreaOffset: 5, //px (space between root tile and display boundary)
+	ancestryBarSz: defaultLytOpts.minTileSz * 2, //px (breadth of ancestry-bar area)
+	ancestryBarBgColor: '#44403c',
+	ancestryTileMargin: 5, //px (gap between detached-ancestor tiles)
+	ancestryBarScrollGap: 10, //px (gap for ancestry-bar scrollbar, used to prevent overlap with tiles)
 	infoModalImgSz: 200,
+	autoWaitTime: 500, //ms (time to wait between actions (with their transitions))
 	// Timing related
-	transitionDuration: 300, //ms
+	tileChgDuration: 300, //ms (for tile move/expand/collapse)
 	clickHoldDuration: 400, //ms (duration after mousedown when a click-and-hold is recognised)
 };
-const defaultOwnOptions = {
-	tileAreaOffset: 5, //px (space between root tile and display boundary)
-	ancestryBarSz: defaultLytOpts.minTileSz * 2, //px (breadth of ancestry-bar area)
-};
-
-// Type representing auto-mode actions
-type Action = 'move across' | 'move down' | 'move up' | 'expand' | 'collapse' | 'expand to view' | 'expand ancestry bar';
-// Used in auto-mode to help avoid action cycles
-function getReverseAction(action: Action): Action | null {
-	switch (action){
-		case 'move across':
-			return null;
-		case 'move down':
-			return 'move up';
-		case 'move up':
-			return 'move down';
-		case 'expand':
-			return 'collapse';
-		case 'collapse':
-			return 'expand';
-		case 'expand to view':
-			return 'expand ancestry bar';
-		case 'expand ancestry bar':
-			return 'expand to view';
-	}
-}
 
-// Holds a tree structure representing a subtree of 'tol' to be rendered
-// Collects events about tile expansion/collapse and window-resize, and initiates relayout of tiles
 export default defineComponent({
 	data(){
 		let layoutTree = initLayoutTree(tol, 0);
 		return {
 			layoutTree: layoutTree,
-			activeRoot: layoutTree,
+			activeRoot: layoutTree, // Differs from layoutTree root when expand-to-view is used
 			layoutMap: initLayoutMap(layoutTree), // Maps names to LayoutNode objects
 			tolMap: tolMap, // Maps names to TolNode objects
-			//
-			infoModalNode: null as TolNode | null, // Hides/unhides info modal, and provides the node to display
+			// Modals and settings related
+			infoModalNode: null as TolNode | null, // Node to display info for, or null
+			helpOpen: false,
 			searchOpen: false,
 			settingsOpen: false,
+			// For search and auto-mode
+			modeRunning: false,
 			lastFocused: null as LayoutNode | null,
-			animationActive: false,
-			autoWaitTime: 500, //ms (in auto mode, time to wait after an action ends)
-			autoPrevAction: null as Action | null, // Used in auto-mode for reducing action cycles
-			autoPrevActionFail: false, // Used in auto-mode to avoid re-trying a failed expand/collapse
-			helpOpen: false,
+			// For auto-mode
+			autoPrevAction: null as Action | null, // Used to help prevent action cycles
+			autoPrevActionFail: false, // Used to avoid re-trying a failed expand/collapse
 			// Options
 			lytOpts: {...defaultLytOpts},
 			uiOpts: {...defaultUiOpts},
-			...defaultOwnOptions,
 			// For window-resize handling
 			width: document.documentElement.clientWidth,
 			height: document.documentElement.clientHeight,
@@ -117,9 +117,10 @@ export default defineComponent({
 		};
 	},
 	computed: {
-		wideArea(): boolean{
+		wideArea(): boolean {
 			return this.width >= this.height;
 		},
+		// Nodes to show in ancestry-bar, with tol root first
 		detachedAncestors(): LayoutNode[] | null {
 			if (this.activeRoot == this.layoutTree){
 				return null;
@@ -132,166 +133,114 @@ export default defineComponent({
 			}
 			return ancestors.reverse();
 		},
+		// Placement info for Tile and AncestryBar
 		tileAreaPos(){
-			let pos = [this.tileAreaOffset, this.tileAreaOffset] as [number, number];
+			let pos = [this.uiOpts.tileAreaOffset, this.uiOpts.tileAreaOffset] as [number, number];
 			if (this.detachedAncestors != null){
 				if (this.wideArea){
-					pos[0] += this.ancestryBarSz;
+					pos[0] += this.uiOpts.ancestryBarSz;
 				} else {
-					pos[1] += this.ancestryBarSz;
+					pos[1] += this.uiOpts.ancestryBarSz;
 				}
 			}
 			return pos;
 		},
 		tileAreaDims(){
 			let dims = [
-				this.width - this.tileAreaOffset*2,
-				this.height - this.tileAreaOffset*2
+				this.width - this.uiOpts.tileAreaOffset*2,
+				this.height - this.uiOpts.tileAreaOffset*2
 			] as [number, number];
 			if (this.detachedAncestors != null){
 				if (this.wideArea){
-					dims[0] -= this.ancestryBarSz;
+					dims[0] -= this.uiOpts.ancestryBarSz;
 				} else {
-					dims[1] -= this.ancestryBarSz;
+					dims[1] -= this.uiOpts.ancestryBarSz;
 				}
 			}
 			return dims;
 		},
 		ancestryBarDims(): [number, number] {
 			if (this.wideArea){
-				return [this.ancestryBarSz, this.height];
+				return [this.uiOpts.ancestryBarSz, this.height];
 			} else {
-				return [this.width, this.ancestryBarSz];
+				return [this.width, this.uiOpts.ancestryBarSz];
 			}
 		},
-		styles(): Record<string,string> {
-			return {
-				position: 'absolute',
-				left: '0',
-				top: '0',
-				width: '100vw', // Making this dynamic causes white flashes when resizing
-				height: '100vh',
-				backgroundColor: '#292524',
-				overflow: 'hidden',
-			};
-		},
 	},
 	methods: {
-		onResize(){
-			if (!this.resizeThrottled){
-				this.width = document.documentElement.clientWidth;
-				this.height = document.documentElement.clientHeight;
-				tryLayout(this.activeRoot, this.layoutMap,
-					this.tileAreaPos, this.tileAreaDims, this.lytOpts, true);
-				// Prevent re-triggering until after a delay
-				this.resizeThrottled = true;
-				setTimeout(() => {this.resizeThrottled = false;}, this.resizeDelay);
-			}
-		},
 		// For tile expand/collapse events
-		onInnerLeafClicked(layoutNode: LayoutNode){
-			let success = tryLayout(this.activeRoot, this.layoutMap,
-				this.tileAreaPos, this.tileAreaDims, this.lytOpts, false, {type: 'expand', node: layoutNode});
+		onLeafClick(layoutNode: LayoutNode){
+			let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+				{allowCollapse: false, chg: {type: 'expand', node: layoutNode}, layoutMap: this.layoutMap});
 			if (!success){
-				layoutNode.expandFailFlag = !layoutNode.expandFailFlag; // Triggers failure animation
+				layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
 			}
 			return success;
 		},
-		onInnerHeaderClicked(layoutNode: LayoutNode){
-			let oldChildren = layoutNode.children;
-			let success = tryLayout(this.activeRoot, this.layoutMap,
-				this.tileAreaPos, this.tileAreaDims, this.lytOpts, false, {type: 'collapse', node: layoutNode});
+		onNonleafClick(layoutNode: LayoutNode){
+			let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+				{allowCollapse: false, chg: {type: 'collapse', node: layoutNode}, layoutMap: this.layoutMap});
 			if (!success){
-				layoutNode.collapseFailFlag = !layoutNode.collapseFailFlag; // Triggers failure animation
+				layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
 			}
 			return success;
 		},
-		// For expand-to-view events
-		onInnerLeafClickHeld(layoutNode: LayoutNode){
+		// For expand-to-view and ancestry-bar events
+		onLeafClickHeld(layoutNode: LayoutNode){
 			if (layoutNode == this.activeRoot){
-				console.log('Ignored expand-to-view on root node');
+				console.log('Ignored expand-to-view on active-root node');
 				return;
 			}
 			LayoutNode.hideUpward(layoutNode);
 			this.activeRoot = layoutNode;
-			tryLayout(this.activeRoot, this.layoutMap,
-				this.tileAreaPos, this.tileAreaDims, this.lytOpts, true, {type: 'expand', node: layoutNode});
+			tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+				{allowCollapse: true, chg: {type: 'expand', node: layoutNode}, layoutMap: this.layoutMap});
 		},
-		onInnerHeaderClickHeld(layoutNode: LayoutNode){
+		onNonleafClickHeld(layoutNode: LayoutNode){
 			if (layoutNode == this.activeRoot){
 				console.log('Ignored expand-to-view on active-root node');
 				return;
 			}
 			LayoutNode.hideUpward(layoutNode);
 			this.activeRoot = layoutNode;
-			tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.lytOpts, true);
+			tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+				{allowCollapse: true, layoutMap: this.layoutMap, });
 		},
-		onDetachedAncestorClicked(layoutNode: LayoutNode){
+		onDetachedAncestorClick(layoutNode: LayoutNode){
 			LayoutNode.showDownward(layoutNode);
 			this.activeRoot = layoutNode;
-			tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.lytOpts, true);
+			tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+				{allowCollapse: true, layoutMap: this.layoutMap});
 		},
-		// For info modal events
-		onInnerInfoIconClicked(node: LayoutNode){
-			this.closeModesAndSettings();
+		// For tile-info events
+		onInfoIconClick(node: LayoutNode){
+			this.resetMode();
 			this.infoModalNode = node.tolNode;
 		},
-		onInfoModalClose(){
-			this.infoModalNode = null;
-		},
-		//
-		onSettingsIconClick(){
-			this.closeModesAndSettings();
-			this.settingsOpen = true;
-		},
-		onSettingsClose(){
-			this.settingsOpen = false;
-		},
-		onLayoutOptionChange(){
-			tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.lytOpts, true);
+		// For help events
+		onHelpIconClick(){
+			this.resetMode();
+			this.helpOpen = true;
 		},
-		//
+		// For search events
 		onSearchIconClick(){
-			this.closeModesAndSettings();
+			this.resetMode();
 			this.searchOpen = true;
 		},
-		onSearchClose(){
-			this.searchOpen = false;
-		},
 		onSearchNode(tolNode: TolNode){
 			this.searchOpen = false;
-			this.animationActive = true;
+			this.modeRunning = true;
 			this.expandToTolNode(tolNode);
 		},
-		//
-		closeModesAndSettings(){
-			this.infoModalNode = null;
-			this.searchOpen = false;
-			this.helpOpen = false;
-			this.settingsOpen = false;
-			this.animationActive = false;
-			this.setLastFocused(null);
-		},
-		onKeyUp(evt: KeyboardEvent){
-			if (evt.key == 'Escape'){
-				this.closeModesAndSettings();
-			} else if (evt.key == 'F' && evt.ctrlKey){
-				if (!this.searchOpen){
-					this.onSearchIconClick();
-				} else {
-					(this.$refs.searchModal as InstanceType<typeof SearchModal>).focusInput();
-				}
-			}
-		},
 		expandToTolNode(tolNode: TolNode){
-			if (!this.animationActive){
+			if (!this.modeRunning){
 				return;
 			}
-			// Check if searched node is shown
+			// Check if searched node is displayed
 			let layoutNode = this.layoutMap.get(tolNode.name);
 			if (layoutNode != null && !layoutNode.hidden){
 				this.setLastFocused(layoutNode);
-				this.animationActive = false;
+				this.modeRunning = false;
 				return;
 			}
 			// Get nearest in-layout-tree ancestor
@@ -300,28 +249,27 @@ export default defineComponent({
 				ancestor = ancestor.parent!;
 			}
 			layoutNode = this.layoutMap.get(ancestor.name)!;
-			// If hidden, expand ancestor in ancestry-bar
+			// If hidden, expand self/ancestor in ancestry-bar
 			if (layoutNode.hidden){
-				// Get self/ancestor in ancestry-bar
 				while (!this.detachedAncestors!.includes(layoutNode)){
 					ancestor = ancestor.parent!;
 					layoutNode = this.layoutMap.get(ancestor.name)!;
 				}
-				this.onDetachedAncestorClicked(layoutNode!);
-				setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.transitionDuration);
+				this.onDetachedAncestorClick(layoutNode!);
+				setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration);
 				return;
 			}
 			// Attempt tile-expand
-			let success = this.onInnerLeafClicked(layoutNode);
+			let success = this.onLeafClick(layoutNode);
 			if (success){
-				setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.transitionDuration);
+				setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration);
 				return;
 			}
 			// Attempt expand-to-view on ancestor just below activeRoot
 			if (ancestor.name == this.activeRoot.tolNode.name){
 				console.log('Unable to complete search (not enough room to expand active root)');
-					// Happens if screen is very small or node has very many children
-				this.animationActive = false;
+					// Note: Only happens if screen is significantly small or node has significantly many children
+				this.modeRunning = false;
 				return;
 			}
 			while (true){
@@ -331,19 +279,17 @@ export default defineComponent({
 				ancestor = ancestor.parent!;
 			}
 			layoutNode = this.layoutMap.get(ancestor.name)!;
-			this.onInnerHeaderClickHeld(layoutNode);
-			setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.transitionDuration);
-		},
-		onOverlayClick(){
-			this.animationActive = false;
+			this.onNonleafClickHeld(layoutNode);
+			setTimeout(() => this.expandToTolNode(tolNode), this.uiOpts.tileChgDuration);
 		},
+		// For auto-mode events
 		onPlayIconClick(){
-			this.closeModesAndSettings();
-			this.animationActive = true;
+			this.resetMode();
+			this.modeRunning = true;
 			this.autoAction();
 		},
 		autoAction(){
-			if (!this.animationActive){
+			if (!this.modeRunning){
 				this.setLastFocused(null);
 				return;
 			}
@@ -356,14 +302,14 @@ export default defineComponent({
 					layoutNode = layoutNode.children[idx!];
 				}
 				this.setLastFocused(layoutNode);
-				setTimeout(this.autoAction, this.autoWaitTime);
+				setTimeout(this.autoAction, this.uiOpts.autoWaitTime);
 			} else {
 				// Determine available actions
 				let action: Action | null;
 				let actionWeights: {[key: string]: number}; // Maps actions to choice weights
 				let node = this.lastFocused;
 				if (node.children.length == 0){
-					actionWeights = {'move across': 1, 'move up': 2, 'expand': 4};
+					actionWeights = {'move across': 1, 'move up': 2, 'expand': 3};
 					// Zero weights for disallowed actions
 					if (node == this.activeRoot || node.parent!.children.length == 1){
 						actionWeights['move across'] = 0;
@@ -377,7 +323,7 @@ export default defineComponent({
 				} else {
 					actionWeights = {
 						'move across': 1, 'move down': 2, 'move up': 1,
-						'collapse': 1, 'expand to view': 0.5, 'expand ancestry bar': 0.5
+						'collapse': 1, 'expand to view': 1, 'expand ancestry bar': 1
 					};
 					// Zero weights for disallowed actions
 					if (node == this.activeRoot || node.parent!.children.length == 1){
@@ -416,12 +362,12 @@ export default defineComponent({
 				// Perform action
 				this.autoPrevActionFail = false;
 				switch (action){
-					case 'move across':
+					case 'move across': // Bias towards siblings with higher dCount
 						let siblings = node.parent!.children.filter(n => n != node);
 						let siblingWeights = siblings.map(n => n.dCount + 1);
 						this.setLastFocused(siblings[randWeightedChoice(siblingWeights)!]);
 						break;
-					case 'move down':
+					case 'move down': // Bias towards children with higher dCount
 						let childWeights = node.children.map(n => n.dCount + 1);
 						this.setLastFocused(node.children[randWeightedChoice(childWeights)!]);
 						break;
@@ -429,22 +375,63 @@ export default defineComponent({
 						this.setLastFocused(node.parent!);
 						break;
 					case 'expand':
-						this.autoPrevActionFail = !this.onInnerLeafClicked(node);
+						this.autoPrevActionFail = !this.onLeafClick(node);
 						break;
 					case 'collapse':
-						this.autoPrevActionFail = !this.onInnerHeaderClicked(node);
+						this.autoPrevActionFail = !this.onNonleafClick(node);
 						break;
 					case 'expand to view':
-						this.onInnerHeaderClickHeld(node);
+						this.onNonleafClickHeld(node);
 						break;
 					case 'expand ancestry bar':
-						this.onDetachedAncestorClicked(node.parent!);
+						this.onDetachedAncestorClick(node.parent!);
 						break;
 				}
-				setTimeout(this.autoAction, this.uiOpts.transitionDuration + this.autoWaitTime);
+				setTimeout(this.autoAction, this.uiOpts.tileChgDuration + this.uiOpts.autoWaitTime);
 				this.autoPrevAction = action;
 			}
 		},
+		// For settings events
+		onSettingsIconClick(){
+			this.resetMode();
+			this.settingsOpen = true;
+		},
+		onLayoutOptionChange(){
+			tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+				{allowCollapse: true, layoutMap: this.layoutMap});
+		},
+		// For other events
+		onResize(){
+			if (!this.resizeThrottled){
+				this.width = document.documentElement.clientWidth;
+				this.height = document.documentElement.clientHeight;
+				tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, 
+					{allowCollapse: true, layoutMap: this.layoutMap});
+				// Prevent re-triggering until after a delay
+				this.resizeThrottled = true;
+				setTimeout(() => {this.resizeThrottled = false;}, this.resizeDelay);
+			}
+		},
+		onKeyUp(evt: KeyboardEvent){
+			if (evt.key == 'Escape'){
+				this.resetMode();
+			} else if (evt.key == 'F' && evt.ctrlKey){ // On ctrl-shift-f
+				if (!this.searchOpen){
+					this.onSearchIconClick();
+				} else {
+					(this.$refs.searchModal as InstanceType<typeof SearchModal>).focusInput();
+				}
+			}
+		},
+		// Helper methods
+		resetMode(){
+			this.infoModalNode = null;
+			this.searchOpen = false;
+			this.helpOpen = false;
+			this.settingsOpen = false;
+			this.modeRunning = false;
+			this.setLastFocused(null);
+		},
 		setLastFocused(node: LayoutNode | null){
 			if (this.lastFocused != null){
 				this.lastFocused.hasFocus = false;
@@ -454,65 +441,64 @@ export default defineComponent({
 				node.hasFocus = true;
 			}
 		},
-		onHelpIconClick(){
-			this.closeModesAndSettings();
-			this.helpOpen = true;
-		},
-		onHelpModalClose(){
-			this.helpOpen = false;
-		},
 	},
 	created(){
 		window.addEventListener('resize', this.onResize);
 		window.addEventListener('keyup', this.onKeyUp);
-		tryLayout(this.activeRoot, this.layoutMap, this.tileAreaPos, this.tileAreaDims, this.lytOpts, true);
+		tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
+			{allowCollapse: true, layoutMap: this.layoutMap});
 	},
 	unmounted(){
 		window.removeEventListener('resize', this.onResize);
 		window.removeEventListener('keyup', this.onKeyUp);
 	},
 	components: {
-		Tile, AncestryBar, TileInfoModal, SettingsPane, SearchModal, HelpModal,
-		SearchIcon, PlayIcon, HelpIcon, SettingsIcon,
+		Tile, AncestryBar,
+		HelpIcon, SearchIcon, PlayIcon, SettingsIcon,
+		TileInfoModal, HelpModal, SearchModal, SettingsPane,
 	},
 });
 </script>
 
 <template>
-<div :style="styles">
+<div class="absolute left-0 top-0 w-screen h-screen overflow-hidden" :style="{backgroundColor: uiOpts.appBgColor}">
+	<!-- Note: Making the above enclosing div's width/height dynamic seems to cause white flashes when resizing -->
 	<tile :layoutNode="layoutTree" :lytOpts="lytOpts" :uiOpts="uiOpts"
-		@leaf-clicked="onInnerLeafClicked" @header-clicked="onInnerHeaderClicked"
-		@leaf-click-held="onInnerLeafClickHeld" @header-click-held="onInnerHeaderClickHeld"
-		@info-icon-clicked="onInnerInfoIconClicked"/>
+		@leaf-click="onLeafClick" @nonleaf-click="onNonleafClick"
+		@leaf-click-held="onLeafClickHeld" @nonleaf-click-held="onNonleafClickHeld"
+		@info-icon-click="onInfoIconClick"/>
 	<ancestry-bar v-if="detachedAncestors != null"
 		:pos="[0,0]" :dims="ancestryBarDims" :nodes="detachedAncestors"
 		:lytOpts="lytOpts" :uiOpts="uiOpts"
-		@detached-ancestor-clicked="onDetachedAncestorClicked" @info-icon-clicked="onInnerInfoIconClicked"/>
+		@detached-ancestor-click="onDetachedAncestorClick" @info-icon-click="onInfoIconClick"/>
 	<!-- Icons -->
 	<help-icon @click="onHelpIconClick"
-		class="absolute bottom-[6px] left-[6px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/>
+		class="absolute bottom-[6px] left-[6px] w-[18px] h-[18px]
+			text-white/40 hover:text-white hover:cursor-pointer"/>
 	<search-icon @click="onSearchIconClick"
-		class="absolute bottom-[6px] left-[30px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/>
+		class="absolute bottom-[6px] left-[30px] w-[18px] h-[18px]
+			text-white/40 hover:text-white hover:cursor-pointer"/>
 	<play-icon @click="onPlayIconClick"
-		class="absolute bottom-[6px] left-[54px] w-[18px] h-[18px] text-white/40 hover:text-white hover:cursor-pointer"/>
+		class="absolute bottom-[6px] left-[54px] w-[18px] h-[18px]
+			text-white/40 hover:text-white hover:cursor-pointer"/>
 	<!-- Modals -->
 	<transition name="fade">
 		<tile-info-modal v-if="infoModalNode != null" :tolNode="infoModalNode" :uiOpts="uiOpts"
-			@info-modal-close="onInfoModalClose"/>
+			@info-modal-close="infoModalNode = null"/>
 	</transition>
 	<transition name="fade">
-		<search-modal v-if="searchOpen" :layoutTree="layoutTree" :tolMap="tolMap" :uiOpts="uiOpts"
-			@search-close="onSearchClose" @search-node="onSearchNode" ref="searchModal"/>
+		<search-modal v-if="searchOpen" :tolMap="tolMap" :uiOpts="uiOpts"
+			@search-close="searchOpen = false" @search-node="onSearchNode" ref="searchModal"/>
 	</transition>
 	<transition name="fade">
-		<help-modal v-if="helpOpen" :uiOpts="uiOpts" @help-modal-close="onHelpModalClose"/>
+		<help-modal v-if="helpOpen" :uiOpts="uiOpts" @help-modal-close="helpOpen = false"/>
 	</transition>
 	<!-- Settings -->
 	<transition name="slide-bottom-right">
 		<settings-pane v-if="settingsOpen" :lytOpts="lytOpts" :uiOpts="uiOpts"
-			@settings-close="onSettingsClose" @layout-option-change="onLayoutOptionChange"/>
-		<!-- outer div prevents transition interference with inner rotate -->
+			@settings-close="settingsOpen = false" @layout-option-change="onLayoutOptionChange"/>
 		<div v-else class="absolute bottom-0 right-0 w-[100px] h-[100px] invisible">
+			<!-- Note: Above enclosing div prevents transition interference with inner rotate -->
 			<div class="absolute bottom-[-50px] right-[-50px] w-[100px] h-[100px] visible -rotate-45
 				bg-black text-white hover:cursor-pointer" @click="onSettingsIconClick">
 				<settings-icon class="w-6 h-6 mx-auto mt-2"/>
@@ -520,46 +506,12 @@ export default defineComponent({
 		</div>
 	</transition>
 	<!-- Overlay used to prevent interaction and capture clicks -->
-	<div :style="{visibility: animationActive ? 'visible' : 'hidden'}"
-		class="absolute left-0 top-0 w-full h-full" @click="onOverlayClick"></div>
+	<div :style="{visibility: modeRunning ? 'visible' : 'hidden'}"
+		class="absolute left-0 top-0 w-full h-full" @click="modeRunning = false"></div>
 </div>
 </template>
 
 <style>
-.animate-expand-shrink {
-	animation-name: expand-shrink;
-	animation-duration: 300ms;
-	animation-iteration-count: 1;
-	animation-timing-function: ease-in-out;
-}
-@keyframes expand-shrink {
-	from {
-		transform: scale(1, 1);
-	}
-	50% {
-		transform: scale(1.1, 1.1);
-	}
-	to {
-		transform: scale(1, 1);
-	}
-}
-.animate-shrink-expand {
-	animation-name: shrink-expand;
-	animation-duration: 300ms;
-	animation-iteration-count: 1;
-	animation-timing-function: ease-in-out;
-}
-@keyframes shrink-expand {
-	from {
-		transform: translate3d(0,0,0) scale(1, 1);
-	}
-	50% {
-		transform: translate3d(0,0,0) scale(0.9, 0.9);
-	}
-	to {
-		transform: translate3d(0,0,0) scale(1, 1);
-	}
-}
 .fade-enter-from, .fade-leave-to {
 	opacity: 0;
 }
-- 
cgit v1.2.3

