diff --git a/package.json b/package.json index 1bd4509..386d4af 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@typescript-eslint/parser": "^6.7.0", "@vitejs/plugin-vue": "^4.3.4", "@vueuse/core": "^10.4.1", - "bndr-js": "^0.10.0", + "bndr-js": "^0.11.1", "case": "^1.6.3", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", diff --git a/src/components/App.vue b/src/components/App.vue index a64f696..e81ade0 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -360,45 +360,45 @@ window.addEventListener('drop', async e => { :view-transform="viewTransform" /> - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/src/tweeq/FloatingPane/FloatingPane.vue b/src/tweeq/FloatingPane/FloatingPane.vue index 405f7db..621d8dd 100644 --- a/src/tweeq/FloatingPane/FloatingPane.vue +++ b/src/tweeq/FloatingPane/FloatingPane.vue @@ -5,37 +5,59 @@ import {computed, onMounted, ref, watch} from 'vue' import {useAppStorage} from '../useAppStorage' +type PaneDimension = number | 'minimized' + +type Position = + | {anchor: 'maximized'} + | {anchor: 'top'; height: PaneDimension} + | {anchor: 'left-top'; width: PaneDimension; height: PaneDimension} + | {anchor: 'left'; width: PaneDimension} + | {anchor: 'left-bottom'; width: PaneDimension; height: PaneDimension} + | {anchor: 'bottom'; height: PaneDimension} + | {anchor: 'right-bottom'; width: PaneDimension; height: PaneDimension} + | {anchor: 'right'; width: PaneDimension} + | {anchor: 'right-top'; width: PaneDimension; height: PaneDimension} + interface Props { name: string icon: string + position?: Position } -const props = defineProps() - -type FloatingWidth = number | 'fill' | 'minimized' -type FloatingHeight = number | 'fill' +const props = withDefaults(defineProps(), { + position: () => { + return { + anchor: 'right-top', + width: 400, + height: 400, + } + }, +}) -const minimizeThreshold = 150 -const minHeight = 200 +const minimizeThreshold = 90 const resizeWidth = 12 const appStorage = useAppStorage() -const width = appStorage(`${props.name}.width`, 400) -const height = appStorage(`${props.name}.height`, 400) +const position = appStorage(`${props.name}.position`, props.position) const windowSize = useWindowSize() const classes = computed(() => { + const p = position.value return { - minimized: width.value === 'minimized', - 'w-fill': width.value === 'fill', - 'h-fill': height.value === 'fill', + 'anchor-maximized': p.anchor === 'maximized', + 'anchor-top': p.anchor.includes('top'), + 'anchor-right': p.anchor.includes('right'), + 'anchor-bottom': p.anchor.includes('bottom'), + 'anchor-left': p.anchor.includes('left'), + 'w-minimized': 'width' in p && p.width === 'minimized', + 'h-minimized': 'height' in p && p.height === 'minimized', } }) const style = computed(() => { - const w = width.value - const h = height.value + const w = 'width' in position.value ? position.value.width : null + const h = 'height' in position.value ? position.value.height : null return { width: typeof w === 'number' ? w + 'px' : '', height: typeof h === 'number' ? h + 'px' : '', @@ -43,77 +65,147 @@ const style = computed(() => { }) const $root = ref(null) +const $top = ref(null) +const $right = ref(null) const $left = ref(null) const $bottom = ref(null) const bound = useElementBounding($root) watch([windowSize.width, windowSize.height], ([ww, wh]) => { - if (typeof width.value === 'number' && width.value > ww) { - width.value = ww + const p = position.value + + if ('width' in p && typeof p.width === 'number' && p.width > ww) { + position.value = {...p, width: ww} } - if (typeof height.value === 'number' && height.value > wh) { - height.value = wh + if ('height' in p && typeof p.height === 'number' && p.height > wh) { + position.value = {...p, height: wh} } }) onMounted(() => { - if (!$left.value || !$bottom.value) return + if (!$top.value || !$right.value || !$left.value || !$bottom.value) return - let wOrigin = 0 Bndr.pointer($left.value) .drag({pointerCapture: true, preventDefault: true}) - .on(e => { - if (e.justStarted) { - if (width.value === 'fill') { - wOrigin = window.innerWidth - } else if (width.value === 'minimized') { - wOrigin = bound.width.value - } else { - wOrigin = width.value - } + .on(e => onDragHoriz(e, true)) + + Bndr.pointer($right.value) + .drag({pointerCapture: true, preventDefault: true}) + .on(e => onDragHoriz(e, false)) + + let widthAtDragStart = 0 + + function onDragHoriz(e: Bndr.DragData, isLeft: boolean) { + const p = position.value + + if (e.justStarted) { + widthAtDragStart = bound.width.value + } + + const dir = isLeft ? -1 : 1 + const current = Math.round( + widthAtDragStart + (e.current[0] - e.start[0]) * dir + ) + + if (current <= minimizeThreshold) { + if ('width' in p) { + position.value = {...p, width: 'minimized'} } - const current = wOrigin - (e.current[0] - e.start[0]) - - if (current <= minimizeThreshold) { - width.value = 'minimized' - } else if (current >= window.innerWidth - resizeWidth) { - width.value = 'fill' - } else { - width.value = current + } else if (current >= window.innerWidth - resizeWidth) { + if (p.anchor === 'right' || p.anchor === 'left') { + position.value = {anchor: 'maximized'} + } else if (p.anchor === 'right-top' || p.anchor === 'left-top') { + position.value = {anchor: 'top', height: p.height} + } else if (p.anchor === 'right-bottom' || p.anchor === 'left-bottom') { + position.value = {anchor: 'bottom', height: p.height} } - }) + } else if ('width' in p) { + position.value = {...p, width: current} + } else { + const opposite = isLeft ? 'right' : 'left' + if (p.anchor === 'maximized') { + position.value = { + anchor: opposite, + width: current, + } + } else if (p.anchor === 'top' || p.anchor === 'bottom') { + position.value = { + anchor: `${opposite}-${p.anchor}` as any, + width: current, + height: p.height, + } + } + } + } - let hOrigin = 0 - const titleBarAreaHeight = useCssVar('--titlebar-area-height') - const maxHeight = computed( - () => windowSize.height.value - parseFloat(titleBarAreaHeight.value) - ) + let heightAtDragStart = 0 + + const maxHeight = computed(() => { + return ( + windowSize.height.value - + parseFloat(useCssVar('--titlebar-area-height').value) + ) + }) + + Bndr.pointer($top.value) + .drag({pointerCapture: true, preventDefault: true}) + .on(e => onDragVert(e, true)) Bndr.pointer($bottom.value) .drag({pointerCapture: true, preventDefault: true}) - .on(e => { - if (e.justStarted) { - if (height.value === 'fill') { - hOrigin = maxHeight.value - } else { - hOrigin = height.value - } - } - const current = hOrigin + (e.current[1] - e.start[1]) + .on(e => onDragVert(e, false)) + + function onDragVert(e: Bndr.DragData, isTop: boolean) { + const p = position.value + + if (e.justStarted) { + heightAtDragStart = bound.height.value + } + + const dir = isTop ? -1 : 1 + const current = Math.round( + heightAtDragStart + (e.current[1] - e.start[1]) * dir + ) - if (current >= maxHeight.value - resizeWidth) { - height.value = 'fill' - } else { - height.value = Math.max(current, minHeight) + if (current <= minimizeThreshold) { + if ('height' in p) { + position.value = {...p, height: 'minimized'} } - }) + } else if (current >= maxHeight.value - resizeWidth) { + if (p.anchor === 'top' || p.anchor === 'bottom') { + position.value = {anchor: 'maximized'} + } else if (p.anchor === 'right-top' || p.anchor === 'right-bottom') { + position.value = {anchor: 'right', width: p.width} + } else if (p.anchor === 'left-top' || p.anchor === 'left-bottom') { + position.value = {anchor: 'left', width: p.width} + } + } else if ('height' in p) { + position.value = {...p, height: current} + } else { + const opposite = isTop ? 'bottom' : 'top' + if (p.anchor === 'maximized') { + position.value = { + anchor: opposite, + height: current, + } + } else if (p.anchor === 'right' || p.anchor === 'left') { + position.value = { + anchor: `${p.anchor}-${opposite}` as any, + width: p.width, + height: current, + } + } + } + } })