mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 15:12:15 +03:00
New ... menu in the top bar with zoom controls (#9073)
Closes #8614 https://github.com/enso-org/enso/assets/1428930/8b5b47e4-5ce5-4bf0-a42f-0b6c3d7d9323
This commit is contained in:
parent
3a2ca2ae0f
commit
e264189f6a
190
app/gui2/src/components/ExtendedMenu.vue
Normal file
190
app/gui2/src/components/ExtendedMenu.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isDropdownOpen = ref(false)
|
||||
|
||||
const props = defineProps<{
|
||||
zoomLevel: number
|
||||
}>()
|
||||
const emit = defineEmits<{ zoomIn: []; zoomOut: []; fitToAllClicked: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ExtendedMenu">
|
||||
<div class="moreIcon" @pointerdown="isDropdownOpen = !isDropdownOpen"></div>
|
||||
<Transition name="dropdown">
|
||||
<div v-show="isDropdownOpen" class="ExtendedMenuPane">
|
||||
<div class="row">
|
||||
<div class="zoomBar row">
|
||||
<div class="label">Zoom</div>
|
||||
<div class="zoomControl last">
|
||||
<div
|
||||
class="zoomButton minus"
|
||||
title="Decrease zoom"
|
||||
@pointerdown.stop="emit('zoomOut')"
|
||||
/>
|
||||
<span
|
||||
class="zoomScaleLabel"
|
||||
v-text="props.zoomLevel ? props.zoomLevel.toFixed(0) + '%' : '?'"
|
||||
></span>
|
||||
<div
|
||||
class="zoomButton plus"
|
||||
title="increase zoom"
|
||||
@pointerdown.stop="emit('zoomIn')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<SvgIcon
|
||||
name="show_all"
|
||||
class="last showAllIcon"
|
||||
@pointerdown="emit('fitToAllClicked')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ExtendedMenu {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
gap: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: auto;
|
||||
margin-right: 125px;
|
||||
}
|
||||
|
||||
.ExtendedMenu:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-frame-bg);
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.ExtendedMenuPane {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
width: 300px;
|
||||
top: 40px;
|
||||
margin-top: 6px;
|
||||
padding: 4px;
|
||||
right: 0px;
|
||||
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-frame-bg);
|
||||
backdrop-filter: var(--blur-app-bg);
|
||||
}
|
||||
|
||||
.label {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding-left: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.last {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-left: 1px solid var(--color-text);
|
||||
border-right: 1px solid var(--color-text);
|
||||
height: 17px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.zoomControl {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.showAllIcon {
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.zoomScaleLabel {
|
||||
width: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.moreIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
font-size: 24px;
|
||||
font-family: var(--font-code);
|
||||
position: relative;
|
||||
right: -4px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.moreIcon:before {
|
||||
content: '\2807';
|
||||
}
|
||||
|
||||
.zoomButton {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: var(--radius-full);
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.zoomButton:hover {
|
||||
background-color: var(--color-menu-entry-hover-bg);
|
||||
}
|
||||
|
||||
.zoomButton.plus:before,
|
||||
.zoomButton.plus:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: var(--color-text);
|
||||
}
|
||||
|
||||
.zoomButton.plus:before {
|
||||
width: 2px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.zoomButton.plus:after {
|
||||
height: 2px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.zoomButton.minus:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-text);
|
||||
margin: auto 2px;
|
||||
height: 2px;
|
||||
}
|
||||
</style>
|
@ -170,6 +170,29 @@ useEvent(window, 'pointerdown', interactionBindingsHandler, { capture: true })
|
||||
|
||||
onMounted(() => viewportNode.value?.focus())
|
||||
|
||||
function zoomToSelected() {
|
||||
if (!viewportNode.value) return
|
||||
let left = Infinity
|
||||
let top = Infinity
|
||||
let right = -Infinity
|
||||
let bottom = -Infinity
|
||||
const nodesToCenter =
|
||||
nodeSelection.selected.size === 0 ? graphStore.db.nodeIdToNode.keys() : nodeSelection.selected
|
||||
for (const id of nodesToCenter) {
|
||||
const rect = graphStore.vizRects.get(id) ?? graphStore.nodeRects.get(id)
|
||||
if (!rect) continue
|
||||
left = Math.min(left, rect.left)
|
||||
right = Math.max(right, rect.right)
|
||||
top = Math.min(top, rect.top)
|
||||
bottom = Math.max(bottom, rect.bottom)
|
||||
}
|
||||
graphNavigator.panAndZoomTo(
|
||||
Rect.FromBounds(left, top, right, bottom),
|
||||
0.1,
|
||||
Math.max(1, graphNavigator.scale),
|
||||
)
|
||||
}
|
||||
|
||||
const graphBindingsHandler = graphBindings.handler({
|
||||
undo() {
|
||||
projectStore.module?.undoManager.undo()
|
||||
@ -202,26 +225,7 @@ const graphBindingsHandler = graphBindings.handler({
|
||||
})
|
||||
},
|
||||
zoomToSelected() {
|
||||
if (!viewportNode.value) return
|
||||
let left = Infinity
|
||||
let top = Infinity
|
||||
let right = -Infinity
|
||||
let bottom = -Infinity
|
||||
const nodesToCenter =
|
||||
nodeSelection.selected.size === 0 ? graphStore.db.nodeIdToNode.keys() : nodeSelection.selected
|
||||
for (const id of nodesToCenter) {
|
||||
const rect = graphStore.vizRects.get(id) ?? graphStore.nodeRects.get(id)
|
||||
if (!rect) continue
|
||||
left = Math.min(left, rect.left)
|
||||
right = Math.max(right, rect.right)
|
||||
top = Math.min(top, rect.top)
|
||||
bottom = Math.max(bottom, rect.bottom)
|
||||
}
|
||||
graphNavigator.panAndZoomTo(
|
||||
Rect.FromBounds(left, top, right, bottom),
|
||||
0.1,
|
||||
Math.max(1, graphNavigator.scale),
|
||||
)
|
||||
zoomToSelected()
|
||||
},
|
||||
selectAll() {
|
||||
if (keyboardBusy()) return
|
||||
@ -660,10 +664,14 @@ function handleEdgeDrop(source: AstId, position: Vec2) {
|
||||
:breadcrumbs="stackNavigator.breadcrumbLabels.value"
|
||||
:allowNavigationLeft="stackNavigator.allowNavigationLeft.value"
|
||||
:allowNavigationRight="stackNavigator.allowNavigationRight.value"
|
||||
:zoomLevel="100.0 * graphNavigator.scale"
|
||||
@breadcrumbClick="stackNavigator.handleBreadcrumbClick"
|
||||
@back="stackNavigator.exitNode"
|
||||
@forward="stackNavigator.enterNextNodeFromHistory"
|
||||
@execute="onPlayButtonPress()"
|
||||
@fitToAllClicked="zoomToSelected"
|
||||
@zoomIn="graphNavigator.scale *= 1.1"
|
||||
@zoomOut="graphNavigator.scale *= 0.9"
|
||||
/>
|
||||
<PlusButton @pointerdown="interaction.setCurrent(creatingNodeFromButton)" />
|
||||
<Transition>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ExtendedMenu from '@/components/ExtendedMenu.vue'
|
||||
import NavBar from '@/components/NavBar.vue'
|
||||
import type { BreadcrumbItem } from '@/components/NavBreadcrumbs.vue'
|
||||
import ProjectTitle from '@/components/ProjectTitle.vue'
|
||||
@ -12,6 +13,7 @@ const props = defineProps<{
|
||||
mode: string
|
||||
allowNavigationLeft: boolean
|
||||
allowNavigationRight: boolean
|
||||
zoomLevel: number
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
execute: []
|
||||
@ -19,6 +21,9 @@ const emit = defineEmits<{
|
||||
forward: []
|
||||
breadcrumbClick: [index: number]
|
||||
'update:mode': [mode: string]
|
||||
fitToAllClicked: []
|
||||
zoomIn: []
|
||||
zoomOut: []
|
||||
}>()
|
||||
|
||||
const LEFT_PADDING_PX = 11
|
||||
@ -50,6 +55,12 @@ const barStyle = computed(() => {
|
||||
@forward="emit('forward')"
|
||||
@breadcrumbClick="emit('breadcrumbClick', $event)"
|
||||
/>
|
||||
<ExtendedMenu
|
||||
:zoomLevel="props.zoomLevel"
|
||||
@fitToAllClicked="emit('fitToAllClicked')"
|
||||
@zoomIn="emit('zoomIn')"
|
||||
@zoomOut="emit('zoomOut')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -61,5 +72,6 @@ const barStyle = computed(() => {
|
||||
top: 9px;
|
||||
/* FIXME[sb]: Get correct offset from dashboard. */
|
||||
left: 9px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@ -55,8 +55,14 @@ export default function UserBar(props: UserBarProps) {
|
||||
self != null
|
||||
const shouldShowInviteButton =
|
||||
sessionType === authProvider.UserSessionType.full && !shouldShowShareButton
|
||||
const shouldMakeSpaceForExtendedEditorMenu = page === pageSwitcher.Page.editor
|
||||
return (
|
||||
<div className="flex shrink-0 items-center bg-frame backdrop-blur-3xl rounded-full gap-3 h-8 pl-2 pr-0.75 cursor-default pointer-events-auto">
|
||||
<div
|
||||
className={
|
||||
'flex shrink-0 items-center bg-frame backdrop-blur-3xl rounded-full gap-3 h-8 pl-2 pr-0.75 cursor-default pointer-events-auto' +
|
||||
(shouldMakeSpaceForExtendedEditorMenu ? ' mr-10' : '')
|
||||
}
|
||||
>
|
||||
<Button
|
||||
active={isHelpChatOpen}
|
||||
image={ChatIcon}
|
||||
|
Loading…
Reference in New Issue
Block a user