Skip to content

Commit

Permalink
feat: better dnd for tab items
Browse files Browse the repository at this point in the history
  • Loading branch information
CyanSalt committed May 7, 2024
1 parent e69e6e2 commit b534b7d
Show file tree
Hide file tree
Showing 21 changed files with 692 additions and 461 deletions.
2 changes: 0 additions & 2 deletions addons/cleaner/src/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import CleanerLink from './CleanerLink.vue'

export default () => {

commas.ui.addCSSFile('dist/renderer/style.css')

commas.context.provide('preference.item', {
component: CleanerLink,
group: 'about',
Expand Down
229 changes: 174 additions & 55 deletions addons/launcher/src/renderer/LauncherList.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script lang="ts" setup>
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'
import * as commas from 'commas:api/renderer'
import { ipcRenderer } from 'electron'
import { nextTick } from 'vue'
import { nextTick, reactive } from 'vue'
import type { DraggableElementDataLike } from '../../../../src/renderer/utils/draggable'
import type { DraggableElementEventPayload } from '../../../../src/typings/draggable'
import type { TerminalTab, TerminalTabCharacter } from '../../../../src/typings/terminal'
import type { Launcher } from '../../typings/launcher'
import {
getLauncherByTerminalTabCharacter,
getTerminalTabCharacterByLauncher,
getTerminalTabsByLauncher,
moveLauncher,
Expand All @@ -15,17 +19,18 @@ import {
useLaunchers,
} from './launcher'
const { vI18n, VisualIcon, SortableList, TabItem } = commas.ui.vueAssets
const { vI18n, VisualIcon, DraggableElement, DropTarget, DropIndicator, TabItem } = commas.ui.vueAssets
const settings = commas.remote.useSettings()
let movingIndex = $(commas.workspace.useMovingTerminalIndex())
let isGroupSeparating = $(commas.workspace.useTerminalTabGroupSeparating())
const position = $computed(() => settings['terminal.view.tabListPosition'])
const isHorizontal = $computed(() => {
return position === 'top' || position === 'bottom'
})
const tabs = $(commas.workspace.useTerminalTabs())
const launchers = $(useLaunchers())
let searcher = $ref<HTMLInputElement>()
Expand Down Expand Up @@ -53,24 +58,27 @@ const filteredLaunchers = $computed(() => {
interface LauncherItem {
key: string,
tab?: TerminalTab,
index?: number,
character: TerminalTabCharacter,
launcher: Launcher,
}
const launcherItems = $computed(() => {
return filteredLaunchers.flatMap(launcher => {
const tabs = getTerminalTabsByLauncher(launcher)
const launcherTabs = getTerminalTabsByLauncher(launcher)
const character = getTerminalTabCharacterByLauncher(launcher)
return tabs.length
? tabs.map<LauncherItem>(tab => ({ key: [launcher.id, tab.pid].join(':'), tab, character, launcher }))
return launcherTabs.length
? launcherTabs.map<LauncherItem>(tab => ({
key: [launcher.id, tab.pid].join(':'),
tab,
index: commas.workspace.getTerminalTabIndex(tab),
character,
launcher,
}))
: [{ key: launcher.id, character, launcher }]
})
})
const isLauncherSortingDisabled = $computed(() => {
return filteredLaunchers.length !== launchers.length
})
const isAnyActionEnabled = $computed(() => {
if (isActionsVisible) return true
return Boolean(keywords.length) || isEditing
Expand All @@ -90,10 +98,6 @@ async function toggleActions() {
}
}
function sortLaunchers(from: number, to: number) {
moveLauncher(from, to)
}
function toggleEditing() {
isEditing = !isEditing
}
Expand Down Expand Up @@ -138,19 +142,81 @@ function showLauncherMenu(event: MouseEvent) {
commas.workspace.showTabOptions(event, 'launcher')
}
function startMoving(from: number) {
const item = launcherItems[from]
if (!item?.tab) return
movingIndex = commas.workspace.getTerminalTabIndex(item.tab)
interface LauncherDraggableElementData extends DraggableElementDataLike {
launcher?: Launcher,
}
interface DropTargetData extends Record<string | symbol, unknown> {
launcher: Launcher,
}
const draggingEdges = reactive(new Map<string, Edge | null>())
function handleDragStart(args: DraggableElementEventPayload<LauncherDraggableElementData>) {
if (args.source.data.type === 'tab') {
const tab = tabs[args.source.data.index!]
isGroupSeparating = Boolean(tab.group)
}
}
function handleDragStop() {
isGroupSeparating = false
}
function handleDrag(args: DraggableElementEventPayload<LauncherDraggableElementData, DropTargetData>) {
let launcher: Launcher | undefined
if (args.source.data.type === 'tab') {
const tab = tabs[args.source.data.index!]
if (tab.character) {
launcher = getLauncherByTerminalTabCharacter(tab.character)
}
} else if (args.source.data.type === 'launcher') {
launcher = args.source.data.launcher
}
if (launcher) {
draggingEdges.set(args.self.data.launcher.id, commas.ui.extractClosestEdge(args.self.data))
}
}
function handleDragLeave(args: DraggableElementEventPayload<LauncherDraggableElementData, DropTargetData>) {
let launcher: Launcher | undefined
if (args.source.data.type === 'tab') {
const tab = tabs[args.source.data.index!]
if (tab.character) {
launcher = getLauncherByTerminalTabCharacter(tab.character)
}
} else if (args.source.data.type === 'launcher') {
launcher = args.source.data.launcher
}
if (launcher) {
draggingEdges.set(args.self.data.launcher.id, null)
}
}
function stopMoving() {
movingIndex = -1
function handleDrop(args: DraggableElementEventPayload<LauncherDraggableElementData, DropTargetData>) {
let launcher: Launcher | undefined
if (args.source.data.type === 'tab') {
const tab = tabs[args.source.data.index!]
if (tab.character) {
launcher = getLauncherByTerminalTabCharacter(tab.character)
}
} else if (args.source.data.type === 'launcher') {
launcher = args.source.data.launcher
}
if (launcher) {
const edge = commas.ui.extractClosestEdge(args.self.data)
const toEdge = edge === 'top' || edge === 'left'
? 'start'
: (edge === 'bottom' || edge === 'right' ? 'end' : undefined)
console.log(launcher, launchers.indexOf(args.self.data.launcher), toEdge)
moveLauncher(launcher, launchers.indexOf(args.self.data.launcher), toEdge)
draggingEdges.set(args.self.data.launcher.id, null)
}
}
</script>

<template>
<div class="launcher-list">
<div :class="['launcher-list', isHorizontal ? 'horizontal' : 'vertical']">
<template v-if="isHorizontal">
<div class="launcher-folder">
<span class="button menu" @click="showLauncherMenu">
Expand All @@ -170,7 +236,7 @@ function stopMoving() {
:class="['button', 'more', { active: isAnyActionEnabled }]"
@click="toggleActions"
>
<VisualIcon name="lucide-more-vertical" />
<VisualIcon name="lucide-ellipsis-vertical" />
</div>
</div>
<div v-show="isActionsVisible" class="launcher-actions" @click.stop>
Expand All @@ -189,40 +255,58 @@ function stopMoving() {
</span>
</div>
</div>
<SortableList
v-slot="{ value: { tab, character, launcher } }"
:value="launcherItems"
value-key="key"
class="launchers"
:disabled="isLauncherSortingDisabled"
@move="startMoving"
@stop="stopMoving"
@change="sortLaunchers"
<DraggableElement
v-for="{ key, tab, index, character, launcher } in launcherItems"
:key="key"
v-slot="{ mount: draggable }"
:data="{ type: tab ? 'tab' : 'launcher', index, launcher }"
@dragstart="handleDragStart"
@drop="handleDragStop"
>
<TabItem
:tab="tab"
:character="character"
:closable="isEditing"
@click="openLauncher(launcher, { tab })"
@close="closeLauncher(launcher, tab)"
<DropTarget
v-slot="{ mount: dropTarget }"
:data="{ launcher }"
:allowed-edges="isHorizontal ? ['left', 'right'] : ['top', 'bottom']"
@dragenter="handleDrag"
@drag="handleDrag"
@dragleave="handleDragLeave"
@drop="handleDrop"
>
<template v-if="!isEditing" #operations>
<div
class="button launch"
@click.stop="startLauncher(launcher)"
@contextmenu="showLauncherScripts(launcher, $event)"
>
<VisualIcon name="lucide-play" />
</div>
<div
class="button launch-externally"
@click.stop="startLauncherExternally(launcher)"
<div
:ref="dropTarget"
:class="['list-item', { 'drop-to-end': draggingEdges.get(launcher.id) === 'right' || draggingEdges.get(launcher.id) === 'bottom' }]"
>
<DropIndicator
v-if="draggingEdges.get(launcher.id)"
:vertical="isHorizontal"
/>
<TabItem
:ref="draggable"
:tab="tab"
:character="character"
:closable="isEditing"
@click="openLauncher(launcher, { tab })"
@close="closeLauncher(launcher, tab)"
>
<VisualIcon name="lucide-external-link" />
</div>
</template>
</TabItem>
</SortableList>
<template v-if="!isEditing" #operations>
<div
class="button launch"
@click.stop="startLauncher(launcher)"
@contextmenu="showLauncherScripts(launcher, $event)"
>
<VisualIcon name="lucide-play" />
</div>
<div
class="button launch-externally"
@click.stop="startLauncherExternally(launcher)"
>
<VisualIcon name="lucide-external-link" />
</div>
</template>
</TabItem>
</div>
</DropTarget>
</DraggableElement>
<div v-if="isEditing" class="new-launcher" @click="createLauncher">
<VisualIcon name="lucide-plus" />
</div>
Expand All @@ -231,11 +315,47 @@ function stopMoving() {
</template>

<style lang="scss" scoped>
.launcher-list {
display: flex;
gap: 8px;
&.horizontal {
padding: 8px 8px 0;
}
&.vertical {
flex-direction: column;
width: calc(100% - 8px);
padding: 8px 0 8px 8px;
}
}
.list-item {
display: flex;
:deep(.drop-indicator) {
transform: translateX(-4px);
}
&.drop-to-end {
flex-direction: row-reverse;
:deep(.drop-indicator) {
transform: translateX(4px);
}
}
.tab-list.vertical & {
flex-direction: column;
:deep(.drop-indicator) {
transform: translateY(-4px);
}
&.drop-to-end {
flex-direction: column-reverse;
:deep(.drop-indicator) {
transform: translateY(4px);
}
}
}
}
.launcher-folder {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 8px 16px;
padding: 0 8px;
line-height: 16px;
cursor: pointer;
.button {
Expand All @@ -246,7 +366,6 @@ function stopMoving() {
}
.tab-list.horizontal & {
height: var(--min-tab-height);
padding-left: 0;
line-height: var(--min-tab-height);
}
}
Expand Down
17 changes: 10 additions & 7 deletions addons/launcher/src/renderer/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,18 @@ export async function startLauncherExternally(launcher: Launcher) {
return ipcRenderer.invoke('execute', explorer)
}

export function moveLauncher(from: number, to: number) {
export function moveLauncher(launcher: Launcher, index: number, edge?: 'start' | 'end') {
const updated = [...launchers]
const rule = updated[from]
if (from < to) {
updated.splice(to + 1, 0, rule)
updated.splice(from, 1)
const fromIndex = updated.indexOf(launcher)
let targetIndex = index
if (fromIndex < index) {
targetIndex = edge === 'start' ? index - 1 : index
updated.splice(targetIndex + 1, 0, launcher)
updated.splice(fromIndex, 1)
} else {
updated.splice(from, 1)
updated.splice(to, 0, rule)
targetIndex = edge === 'end' ? index + 1 : index
updated.splice(fromIndex, 1)
updated.splice(targetIndex, 0, launcher)
}
launchers = updated
}
Expand Down
10 changes: 8 additions & 2 deletions api/modules/ui.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as path from 'node:path'
import * as url from 'node:url'
import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'
import TabItem from '../../src/renderer/components/TabItem.vue'
import TerminalBlock from '../../src/renderer/components/TerminalBlock.vue'
import TerminalPane from '../../src/renderer/components/TerminalPane.vue'
import DraggableElement from '../../src/renderer/components/basic/DraggableElement.vue'
import DropIndicator from '../../src/renderer/components/basic/DropIndicator.vue'
import DropTarget from '../../src/renderer/components/basic/DropTarget.vue'
import LoadingSpinner from '../../src/renderer/components/basic/LoadingSpinner.vue'
import ObjectEditor from '../../src/renderer/components/basic/ObjectEditor.vue'
import SortableList from '../../src/renderer/components/basic/SortableList.vue'
import SwitchControl from '../../src/renderer/components/basic/SwitchControl.vue'
import ValueSelector from '../../src/renderer/components/basic/ValueSelector.vue'
import VisualIcon from '../../src/renderer/components/basic/VisualIcon.vue'
Expand Down Expand Up @@ -34,7 +37,9 @@ const vueAssets = {
LoadingSpinner,
VisualIcon,
ObjectEditor,
SortableList,
DraggableElement,
DropTarget,
DropIndicator,
SwitchControl,
TerminalBlock,
TerminalPane,
Expand All @@ -49,4 +54,5 @@ export {
addCSSFile,
openContextMenu,
createContextMenu,
extractClosestEdge,
}
Loading

0 comments on commit b534b7d

Please sign in to comment.