aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2022-05-10 11:20:42 +1000
committerTerry Truong <terry06890@gmail.com>2022-05-10 11:40:34 +1000
commit96a8a5ed5b22b78368e25209059866915256cc56 (patch)
tree54075a58e7551d38d9363d8a62927e7fe1cec125 /src
parentf50c22d5303507a5d52be960e978ed57c1106fbb (diff)
Enable display of active-root with overflow
Diffstat (limited to 'src')
-rw-r--r--src/App.vue72
-rw-r--r--src/components/AncestryBar.vue10
-rw-r--r--src/components/Tile.vue46
-rw-r--r--src/layout.ts57
4 files changed, 147 insertions, 38 deletions
diff --git a/src/App.vue b/src/App.vue
index c9d752d..ba13827 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -16,7 +16,7 @@ import SettingsIcon from './components/icon/SettingsIcon.vue';
import type {TolMap} from './tol';
import {TolNode} from './tol';
import {LayoutNode, initLayoutTree, initLayoutMap, tryLayout} from './layout';
-import type {LayoutOptions} from './layout';
+import type {LayoutOptions, LayoutTreeChg} from './layout';
import {arraySum, randWeightedChoice} from './util';
// Note: Import paths lack a .ts or .js extension because .ts makes vue-tsc complain, and .js makes vite complain
@@ -79,10 +79,10 @@ const defaultUiOpts = {
// For other components
appBgColor: '#292524',
tileAreaOffset: 5, //px (space between root tile and display boundary)
+ scrollGap: 12, //px (gap for overflown-root and ancestry-bar scrollbars, used to prevent overlap)
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
@@ -98,6 +98,7 @@ export default defineComponent({
layoutTree: layoutTree,
activeRoot: layoutTree, // Differs from layoutTree root when expand-to-view is used
layoutMap: initLayoutMap(layoutTree), // Maps names to LayoutNode objects
+ overflownRoot: false, // Set when displaying a root tile with many children, with overflow
// Modals and settings related
infoModalNode: null as LayoutNode | null, // Node to display info for, or null
helpOpen: false,
@@ -175,12 +176,30 @@ export default defineComponent({
methods: {
// For tile expand/collapse events
onLeafClick(layoutNode: LayoutNode){
+ // If clicking child of overflowing active-root
+ if (this.overflownRoot){
+ layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
+ return Promise.resolve(false);
+ }
+ // Function for expanding tile
let doExpansion = () => {
- let success = tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, {
+ let lytFnOpts = {
allowCollapse: false,
- chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap},
+ chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap} as LayoutTreeChg,
layoutMap: this.layoutMap
- });
+ };
+ let success = tryLayout(
+ this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, lytFnOpts);
+ // If expanding active-root with too many children to fit, allow overflow
+ if (!success && layoutNode == this.activeRoot){
+ success = tryLayout(this.activeRoot, this.tileAreaPos,
+ [this.tileAreaDims[0] - this.uiOpts.scrollGap, this.tileAreaDims[1]],
+ {...this.lytOpts, layoutType: 'flex-sqr'}, lytFnOpts);
+ if (success){
+ this.overflownRoot = true;
+ }
+ }
+ // Check for failure
if (!success){
layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
}
@@ -211,6 +230,10 @@ export default defineComponent({
if (!success){
layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
} else {
+ // Update overflownRoot if root was collapsed
+ if (this.overflownRoot){
+ this.overflownRoot = false;
+ }
// Clear out excess nodes when a threshold is reached
let numNodes = this.tolMap.size;
let extraNodes = numNodes - this.layoutMap.size;
@@ -228,18 +251,34 @@ export default defineComponent({
// For expand-to-view and ancestry-bar events
onLeafClickHeld(layoutNode: LayoutNode){
if (layoutNode == this.activeRoot){
- console.log('Ignored expand-to-view on active-root node');
+ this.onLeafClick(layoutNode);
return;
}
// Function for expanding tile
let doExpansion = () => {
LayoutNode.hideUpward(layoutNode);
this.activeRoot = layoutNode;
- tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, {
- allowCollapse: true,
- chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap},
+ this.overflownRoot = false;
+ let lytFnOpts = {
+ allowCollapse: false,
+ chg: {type: 'expand', node: layoutNode, tolMap: this.tolMap} as LayoutTreeChg,
layoutMap: this.layoutMap
- });
+ };
+ let success = tryLayout(
+ this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts, lytFnOpts);
+ if (!success){
+ success = tryLayout(this.activeRoot, this.tileAreaPos,
+ [this.tileAreaDims[0] - this.uiOpts.scrollGap, this.tileAreaDims[1]],
+ {...this.lytOpts, layoutType: 'flex-sqr'}, lytFnOpts);
+ if (success){
+ this.overflownRoot = true;
+ }
+ }
+ // Check for failure
+ if (!success){
+ layoutNode.failFlag = !layoutNode.failFlag; // Triggers failure animation
+ }
+ return success;
};
// Check if data for node-to-expand exists, getting from server if needed
let tolNode = this.tolMap.get(layoutNode.name)!;
@@ -272,6 +311,7 @@ export default defineComponent({
this.activeRoot = layoutNode;
tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
{allowCollapse: true, layoutMap: this.layoutMap});
+ this.overflownRoot = false;
},
// For tile-info events
onInfoIconClick(node: LayoutNode){
@@ -324,6 +364,11 @@ export default defineComponent({
return;
}
// Attempt tile-expand
+ if (this.overflownRoot){
+ this.onLeafClickHeld(layoutNode);
+ setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration);
+ return;
+ }
this.onLeafClick(layoutNode).then(success => {
if (success){
setTimeout(() => this.expandToNode(name), this.uiOpts.tileChgDuration);
@@ -331,8 +376,7 @@ export default defineComponent({
}
// Attempt expand-to-view on ancestor just below activeRoot
if (layoutNode == this.activeRoot){
- console.log('Unable to complete search (not enough room to expand active root)');
- // Note: Only happens if screen is significantly small or node has significantly many children
+ console.log('Screen too small to expand active root');
this.modeRunning = false;
return;
}
@@ -381,7 +425,7 @@ export default defineComponent({
if (node == this.activeRoot){
actionWeights['move up'] = 0;
}
- if (this.tolMap.get(node.name)!.children.length == 0){
+ if (this.tolMap.get(node.name)!.children.length == 0 || this.overflownRoot){
actionWeights['expand'] = 0;
}
} else {
@@ -473,6 +517,7 @@ export default defineComponent({
this.height = document.documentElement.clientHeight;
tryLayout(this.activeRoot, this.tileAreaPos, this.tileAreaDims, this.lytOpts,
{allowCollapse: true, layoutMap: this.layoutMap});
+ this.overflownRoot = false;
// Prevent re-triggering until after a delay
this.resizeThrottled = true;
setTimeout(() => {this.resizeThrottled = false;}, this.resizeDelay);
@@ -544,6 +589,7 @@ export default defineComponent({
<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" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ :overflownDim="overflownRoot ? tileAreaDims[1] : 0"
@leaf-click="onLeafClick" @nonleaf-click="onNonleafClick"
@leaf-click-held="onLeafClickHeld" @nonleaf-click-held="onNonleafClickHeld"
@info-icon-click="onInfoIconClick"/>
diff --git a/src/components/AncestryBar.vue b/src/components/AncestryBar.vue
index a156a96..ca865e9 100644
--- a/src/components/AncestryBar.vue
+++ b/src/components/AncestryBar.vue
@@ -24,7 +24,7 @@ export default defineComponent({
},
tileSz(){
return (this.wideArea ? this.dims[1] : this.dims[0]) -
- (this.uiOpts.ancestryTileMargin * 2) - this.uiOpts.ancestryBarScrollGap;
+ (this.uiOpts.ancestryTileMargin * 2) - this.uiOpts.scrollGap;
},
usedNodes(){ // Childless versions of 'nodes' used to parameterise <tile>
return this.nodes.map(n => {
@@ -39,10 +39,10 @@ export default defineComponent({
return len > (this.wideArea ? this.dims[0] : this.dims[1]);
},
width(){
- return this.dims[0] + (this.wideArea || this.overflowing ? 0 : -this.uiOpts.ancestryBarScrollGap);
+ return this.dims[0] + (this.wideArea || this.overflowing ? 0 : -this.uiOpts.scrollGap);
},
height(){
- return this.dims[1] + (!this.wideArea || this.overflowing ? 0 : -this.uiOpts.ancestryBarScrollGap);
+ return this.dims[1] + (!this.wideArea || this.overflowing ? 0 : -this.uiOpts.scrollGap);
},
styles(): Record<string,string> {
return {
@@ -54,8 +54,8 @@ export default defineComponent({
overflowX: this.wideArea ? 'auto' : 'hidden',
overflowY: this.wideArea ? 'hidden' : 'auto',
// Extra padding for scrollbar inclusion
- paddingRight: (this.overflowing && !this.wideArea ? this.uiOpts.ancestryBarScrollGap : 0) + 'px',
- paddingBottom: (this.overflowing && this.wideArea ? this.uiOpts.ancestryBarScrollGap : 0) + 'px',
+ paddingRight: (this.overflowing && !this.wideArea ? this.uiOpts.scrollGap : 0) + 'px',
+ paddingBottom: (this.overflowing && this.wideArea ? this.uiOpts.scrollGap : 0) + 'px',
// For child layout
display: 'flex',
flexDirection: this.wideArea ? 'row' : 'column',
diff --git a/src/components/Tile.vue b/src/components/Tile.vue
index 429973f..dc9c87d 100644
--- a/src/components/Tile.vue
+++ b/src/components/Tile.vue
@@ -15,8 +15,11 @@ export default defineComponent({
// Options
lytOpts: {type: Object as PropType<LayoutOptions>, required: true},
uiOpts: {type: Object, required: true},
- // For a leaf node, prevents usage of absolute positioning (used by AncestryBar)
+ // Other
nonAbsPos: {type: Boolean, default: false},
+ // For a leaf node, prevents usage of absolute positioning (used by AncestryBar)
+ overflownDim: {type: Number, default: 0},
+ // For a non-leaf node, display with overflow within area of this height
},
data(){
return {
@@ -46,6 +49,9 @@ export default defineComponent({
displayName(): string {
return capitalizeWords(this.tolNode.commonName || this.layoutNode.name);
},
+ isOverflownRoot(): boolean {
+ return this.overflownDim > 0 && !this.layoutNode.hidden && this.layoutNode.children.length > 0;
+ },
// Style related
nonleafBgColor(): string {
let colorArray = this.uiOpts.nonleafBgColors;
@@ -68,16 +74,6 @@ export default defineComponent({
width: this.layoutNode.dims[0] + 'px',
height: this.layoutNode.dims[1] + 'px',
visibility: 'visible',
- };
- if (this.layoutNode.hidden){
- layoutStyles.left = layoutStyles.top = layoutStyles.width = layoutStyles.height = '0';
- layoutStyles.visibility = 'hidden';
- }
- if (this.nonAbsPos){
- layoutStyles.position = 'static';
- }
- return {
- ...layoutStyles,
// Transition related
transitionDuration: this.uiOpts.tileChgDuration + 'ms',
transitionProperty: 'left, top, width, height, visibility',
@@ -88,6 +84,19 @@ export default defineComponent({
'--nonleafBgColor': this.nonleafBgColor,
'--tileSpacing': this.lytOpts.tileSpacing + 'px',
};
+ if (this.layoutNode.hidden){
+ layoutStyles.left = layoutStyles.top = layoutStyles.width = layoutStyles.height = '0';
+ layoutStyles.visibility = 'hidden';
+ }
+ if (this.nonAbsPos){
+ layoutStyles.position = 'static';
+ }
+ if (this.isOverflownRoot){
+ layoutStyles.width = (this.layoutNode.dims[0] + this.uiOpts.scrollGap) + 'px';
+ layoutStyles.height = this.overflownDim + 'px';
+ layoutStyles.overflow = 'scroll';
+ }
+ return layoutStyles;
},
leafStyles(): Record<string,string> {
return {
@@ -128,11 +137,20 @@ export default defineComponent({
`${borderR} ${borderR} ${borderR} 0` :
`${borderR} 0 ${borderR} ${borderR}`;
}
- return {
+ let styles = {
+ position: 'static',
+ width: '100%',
+ height: '100%',
backgroundColor: this.nonleafBgColor,
borderRadius: borderR,
boxShadow: this.boxShadow,
};
+ if (this.isOverflownRoot){
+ styles.position = 'absolute';
+ styles.width = this.layoutNode.dims[0] + 'px';
+ styles.height = this.layoutNode.dims[1] + 'px';
+ }
+ return styles;
},
nonleafHeaderStyles(): Record<string,string> {
let borderR = this.uiOpts.borderRadius + 'px';
@@ -327,7 +345,7 @@ export default defineComponent({
class="self-end text-white/10 hover:text-white hover:cursor-pointer"
@click.stop="onInfoIconClick" @mousedown.stop @mouseup.stop/>
</div>
- <div v-else :style="nonleafStyles" class="w-full h-full" ref="nonleaf">
+ <div v-else :style="nonleafStyles" ref="nonleaf">
<div v-if="showNonleafHeader" :style="nonleafHeaderStyles" class="flex hover:cursor-pointer"
@mouseenter="onMouseEnter" @mouseleave="onMouseLeave" @mousedown="onMouseDown" @mouseup="onMouseUp">
<h1 :style="nonleafHeaderTextStyles" class="grow">{{displayName}}</h1>
@@ -345,7 +363,7 @@ export default defineComponent({
</div>
</div>
<tile v-for="child in layoutNode.children" :key="child.name"
- :layoutNode="child" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts"
+ :layoutNode="child" :tolMap="tolMap" :lytOpts="lytOpts" :uiOpts="uiOpts" :overflownDim="overflownDim"
@leaf-click="onInnerLeafClick" @nonleaf-click="onInnerNonleafClick"
@leaf-click-held="onInnerLeafClickHeld" @nonleaf-click-held="onInnerNonleafClickHeld"
@info-icon-click="onInnerInfoIconClick"/>
diff --git a/src/layout.ts b/src/layout.ts
index fe17b01..f5873ca 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -140,7 +140,7 @@ export type LayoutOptions = {
minTileSz: number; // Minimum size of a tile edge, in pixels
maxTileSz: number;
// Layout-algorithm related
- layoutType: 'sqr' | 'rect' | 'sweep'; // The LayoutFn function to use
+ layoutType: 'sqr' | 'rect' | 'sweep' | 'flex-sqr'; // The LayoutFn function to use
rectMode: 'horz' | 'vert' | 'linear' | 'auto' | 'auto first-row';
// Rect-layout in 1 row, 1 column, 1 row or column, or multiple rows (optionally with first-row-heuristic)
sweepMode: 'left' | 'top' | 'shorter' | 'auto'; // Sweep to left, top, shorter-side, or to minimise empty space
@@ -224,14 +224,17 @@ export function tryLayout(
let tempTree = layoutTree.cloneNodeTree(chg);
let success: boolean;
switch (options.layoutType){
- case 'sqr': success = sqrLayout(tempTree, pos, dims, true, allowCollapse, options); break;
- case 'rect': success = rectLayout(tempTree, pos, dims, true, allowCollapse, options); break;
+ case 'sqr': success = sqrLayout(tempTree, pos, dims, true, allowCollapse, options); break;
+ case 'rect': success = rectLayout(tempTree, pos, dims, true, allowCollapse, options); break;
case 'sweep': success = sweepLayout(tempTree, pos, dims, true, allowCollapse, options); break;
+ case 'flex-sqr': success = flexSqrLayout(tempTree, pos, dims, true, allowCollapse, options); break;
}
if (success){
- // Center in layout area
- tempTree.pos[0] += (dims[0] - tempTree.dims[0]) / 2;
- tempTree.pos[1] += (dims[1] - tempTree.dims[1]) / 2;
+ if (options.layoutType != 'flex-sqr'){
+ // Center in layout area
+ tempTree.pos[0] += (dims[0] - tempTree.dims[0]) / 2;
+ tempTree.pos[1] += (dims[1] - tempTree.dims[1]) / 2;
+ }
// Copy to given LayoutNode tree
tempTree.copyTreeForRender(layoutTree, layoutMap);
}
@@ -796,3 +799,45 @@ let sweepLayout: LayoutFn = function (node, pos, dims, showHeader, allowCollapse
node.assignLayoutData(pos, usedDims, {showHeader, empSpc, sepSweptArea: null});
return true;
}
+// Lays out nodes like sqrLayout(), but may extend past the height limit to fit nodes
+// Does not recurse on child nodes with children
+let flexSqrLayout: LayoutFn = function(node, pos, dims, showHeader, allowCollapse, opts){
+ if (node.children.length == 0){
+ return oneSqrLayout(node, pos, dims, false, false, opts);
+ }
+ // Consider area excluding header and top/left spacing
+ let headerSz = showHeader ? opts.headerSz : 0;
+ let newPos = [opts.tileSpacing, opts.tileSpacing + headerSz];
+ let newWidth = dims[0] - opts.tileSpacing;
+ if (newWidth <= 0){
+ return false;
+ }
+ // Find number of rows and columns
+ let numChildren = node.children.length;
+ let maxNumCols = Math.floor(newWidth / (opts.minTileSz + opts.tileSpacing));
+ if (maxNumCols == 0){
+ if (allowCollapse){
+ node.children = [];
+ LayoutNode.updateDCounts(node, 1 - node.dCount);
+ return oneSqrLayout(node, pos, dims, false, false, opts);
+ }
+ return false;
+ }
+ let numCols = Math.min(numChildren, maxNumCols);
+ let numRows = Math.ceil(numChildren / numCols);
+ let tileSz = Math.min(opts.maxTileSz, Math.floor(newWidth / numCols) - opts.tileSpacing);
+ // Layout children
+ for (let i = 0; i < numChildren; i++){
+ let childX = newPos[0] + (i % numCols) * (tileSz + opts.tileSpacing);
+ let childY = newPos[1] + Math.floor(i / numCols) * (tileSz + opts.tileSpacing);
+ oneSqrLayout(node.children[i], [childX,childY], [tileSz,tileSz], false, false, opts);
+ }
+ // Create layout
+ let usedDims: [number, number] = [
+ numCols * (tileSz + opts.tileSpacing) + opts.tileSpacing,
+ numRows * (tileSz + opts.tileSpacing) + opts.tileSpacing + headerSz
+ ];
+ let empSpc = 0; // Intentionally not used
+ node.assignLayoutData(pos, usedDims, {showHeader, empSpc});
+ return true;
+}