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:
Michael Mauderer 2024-02-20 15:44:49 +00:00 committed by GitHub
parent 3a2ca2ae0f
commit e264189f6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 237 additions and 21 deletions

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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}