Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [Add right click menu for multiple components][14640].
- [Execution can be scheduled for the specific version tag][14883]
- [Add component spacing options][14888]
- [When resizing component, other components are moved to make room][14904]

[14590]: https://github.com/enso-org/enso/pull/14590
[14678]: https://github.com/enso-org/enso/pull/14678
Expand All @@ -28,6 +29,7 @@
[14640]: https://github.com/enso-org/enso/pull/14640
[14883]: https://github.com/enso-org/enso/pull/14883
[14888]: https://github.com/enso-org/enso/pull/14888
[14904]: https://github.com/enso-org/enso/pull/14904

#### Enso Standard Library

Expand Down
25 changes: 12 additions & 13 deletions app/gui/src/dashboard/utilities/__tests__/jsonSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,30 +52,29 @@ fc.test.prop({ value: fc.fc.string() })('string schema', ({ value }) => {
})

const NUMBER_SCHEMA = { type: 'number' } as const
fc.test.prop({ value: fc.fc.float() })('number schema', ({ value }) => {
if (Number.isFinite(value)) {
fc.test.prop({ value: fc.fc.float({ noNaN: true, noDefaultInfinity: true }) })(
'number schema',
({ value }) => {
const constSchema = { const: value, type: 'number' }
v.expect(AJV.validate(NUMBER_SCHEMA, value)).toBe(true)
v.expect(AJV.validate(constSchema, value)).toBe(true)
v.expect(jsonSchema.constantValueOfSchema({}, constSchema)[0]).toBe(value)
}
})
},
)

fc.test.prop({
value: fc.fc.float().filter((n) => n > 0),
value: fc.fc.float({ noNaN: true, noDefaultInfinity: true, min: 0, minExcluded: true }),

multiplier: fc.fc.integer({ min: -1_000_000, max: 1_000_000 }),
})('number multiples', ({ value, multiplier }) => {
const schema = { type: 'number', multipleOf: value }
if (Number.isFinite(value)) {
v.expect(AJV.validate(schema, 0)).toBe(true)
v.expect(AJV.validate(schema, value)).toBe(true)
v.expect(AJV.validate(schema, 0)).toBe(true)
v.expect(AJV.validate(schema, value)).toBe(true)

if (Math.abs(value * (multiplier + 0.5)) < Number.MAX_SAFE_INTEGER) {
v.expect(AJV.validate(schema, value * multiplier)).toBe(true)
if (value !== 0) {
v.expect(AJV.validate(schema, value * (multiplier + 0.5))).toBe(false)
}
if (Math.abs(value * (multiplier + 0.5)) < Number.MAX_SAFE_INTEGER) {
v.expect(AJV.validate(schema, value * multiplier)).toBe(true)
if (value !== 0) {
v.expect(AJV.validate(schema, value * (multiplier + 0.5))).toBe(false)
}
}
})
Expand Down
67 changes: 44 additions & 23 deletions app/gui/src/project-view/components/GraphEditor/GraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import GraphNodeMessage from '@/components/GraphEditor/GraphNodeMessage.vue'
import GraphNodeSubmenu from '@/components/GraphEditor/GraphNodeSubmenu.vue'
import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
import { useNodesDisplacing } from '@/components/GraphEditor/nodesDisplacing'
import { useResizeHandles } from '@/components/resizeHandles'
import ResizeHandles from '@/components/ResizeHandles.vue'
import SvgIcon from '@/components/SvgIcon.vue'
Expand Down Expand Up @@ -90,6 +91,8 @@ const nodeExecution = useNodeExecution()
const nodeId = computed(() => asNodeId(props.node.rootExpr.externalId))
const primaryApplication = computed(() => props.node.primaryApplication)

const scale = computed(() => navigator?.scale ?? 1)

const nodePosition = computed(() => {
// Positions of nodes that are not yet placed are set to `Infinity`.
if (props.node.position.equals(Vec2.Infinity)) return Vec2.Zero
Expand All @@ -99,8 +102,25 @@ const nodePosition = computed(() => {
onUnmounted(() => graph.unregisterNodeRect(nodeId.value))

const rootNode = ref<HTMLElement>()
const contentNode = ref<HTMLElement>()
const nodeSize = useResizeObserver(rootNode)
const widgetTreeNode = ref<HTMLElement>()

const widgetsDomSizeClientPx = useResizeObserver(widgetTreeNode, false)
const widgetsDomSize = ref(new Vec2(0, 0))
// Maintain the size in scene px. The values reported by the resize observer are in client px, so they are dependent on
// the scale; however, changes to the scale don't cause resize events--so the resize observer is non-reactively (via the
// DOM) dependent on reactive state. Thus, we must correct for the scale by non-reactively sampling it at the time a
// resize is observed.
watch(widgetsDomSizeClientPx, (size) => (widgetsDomSize.value = size.scale(1 / scale.value)), {
immediate: true,
flush: 'sync',
})
Comment thread
farmaazon marked this conversation as resolved.
// Compute the node's natural size based on the size of its widgets. We measure the widget tree instead of the node
// directly, because measuring the node would cause a cycle:
// - This value is used as in input to determine the size of the visualization.
// - The size of the visualization affects the size of the node.
const nodeDomSize = computed(() =>
widgetsDomSize.value.add(new Vec2(NODE_CONTENT_PADDING * 2, NODE_CONTENT_PADDING * 2)),
)
Comment thread
farmaazon marked this conversation as resolved.

providePopoverRoot(rootNode)

Expand Down Expand Up @@ -180,25 +200,25 @@ function ensureSelected() {

const outputHovered = computed(() => graph.nodeOutputHovered.get(nodeId.value))

const scale = computed(() => navigator?.scale ?? 1)
const nodeRect = computed(() => new Rect(props.node.position, nodeSize.value))

const { displaceNodesForResize } = useNodesDisplacing()
const {
visualizationWidth,
isVisualizationEnabled,
isVisualizationPreviewed,
visRect,
vizHeight,
visualization,
} = useNodeVisualization({
vis: () => props.node.vis,
nodeHovered: () => nodeHovered.value || outputHovered.value,
nodeRect,
nodeWidgetsSize: nodeDomSize,
nodePos: () => props.node.position,
scale,
isFocused: detailedView,
typeinfo: () => expressionInfo.value?.typeInfo,
dataSource: () => ({ type: 'node', nodeId: props.node.rootExpr.externalId }) as const,
hidden: toRef(props, 'edited'),
emit,
onResize: (rect0, rect1) => displaceNodesForResize(nodeId.value, rect0, rect1),
})

watch(isVisualizationPreviewed, (newVal) => {
Expand Down Expand Up @@ -248,15 +268,15 @@ const nodeEditHandler = nodeEditBindings.handler({
edit: () => actionHandlers['component.startEditing'].action(),
})

/// The visualization's contribution to the node's height.
const vizBelowNode = computed(() => (visRect.value ? visRect.value.size.y - nodeSize.value.y : 0))

const nodeOuterRect = ref<Rect>()
let prevNodeRect: Rect | undefined = undefined
watchEffect(() => {
const newValue = visRect.value ?? nodeRect.value
if (!newValue.size.isZero() && !nodeOuterRect.value?.equals(newValue)) {
nodeOuterRect.value = newValue
emit('update:rect', newValue)
if (nodeDomSize.value.isZero()) return
const width = Math.max(nodeDomSize.value.x, visualizationWidth.value)
const height = nodeDomSize.value.y + vizHeight.value
const newRect = new Rect(props.node.position, new Vec2(width, height))
if (!prevNodeRect?.equals(newRect)) {
emit('update:rect', newRect)
prevNodeRect = newRect
}
})

Expand Down Expand Up @@ -286,15 +306,16 @@ function useRecomputation() {
* takes the size of the largest resizable widget present. If the user resizes the node, and the node is in expanded
* mode, the specified height overrides any widget preferences.
*/
const nodeHeight = computed(() => props.node.height)
const nodeHeightOverride = computed(() => props.node.height)
const nodeHeightOverridden = computed(() => props.node.height != null)
const nodeStyle = computed(() => {
return {
transform: transform.value,
minWidth: isVisualizationEnabled.value ? `${visualizationWidth.value ?? 200}px` : undefined,
height: nodeHeight.value ? `${nodeHeight.value}px` : undefined,
minWidth: `${visualizationWidth.value ?? 200}px`,
height: nodeHeightOverride.value ? `${nodeHeightOverride.value}px` : undefined,
'--node-group-color': baseColor.value,
...(props.node.zIndex ? { 'z-index': props.node.zIndex } : {}),
'--viz-below-node': `${vizBelowNode.value}px`,
'--viz-below-node': `${vizHeight.value}px`,
}
})

Expand All @@ -310,7 +331,7 @@ const { progressAnimating, backgroundProgressEvents } = watchProgress()

const showProgressBar = computed(() => nodeProgress.value !== 100 || progressAnimating.value)

const nodeClass = computed(() => {
const nodeClass = computed<Record<string, boolean>>(() => {
return {
selected: selected.value,
pending: pending.value,
Expand All @@ -320,7 +341,7 @@ const nodeClass = computed(() => {
menuVisible: menuVisible.value,
menuFull: menuFull.value,
edited: props.edited,
nodeHeightOverridden: nodeHeight.value != null,
nodeHeightOverridden: nodeHeightOverridden.value,
}
})

Expand Down Expand Up @@ -462,7 +483,7 @@ const nodeName = computed(() => props.node.pattern?.code())
// === Node resizing ===

const resizeHandles = useResizeHandles({
size: nodeSize,
size: nodeDomSize,
scale,
})
resizeHandles.onResizeHeight((value) => emit('update:height', value))
Expand Down Expand Up @@ -517,7 +538,6 @@ resizeHandles.onResizeHeight((value) => emit('update:height', value))
</div>
</template>
<div
ref="contentNode"
:class="{ content: true, dragged: isDragged }"
:style="contentNodeStyle"
v-on="pointerEvents"
Expand All @@ -526,6 +546,7 @@ resizeHandles.onResizeHeight((value) => emit('update:height', value))
@pointermove="updateNodeHover"
>
<ComponentWidgetTree
ref="widgetTreeNode"
:ast="props.node.innerExpr"
:nodeId="nodeId"
:rootElement="rootNode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { type VisualizationDataSource } from '@/stores/visualization'
import { type Opt } from '@/util/data/opt'
import { Rect } from '@/util/data/rect'
import { Vec2 } from '@/util/data/vec2'
import { computed, ref, shallowRef, toValue, watch } from 'vue'
import { computed, ref, toValue, watch } from 'vue'
import type { ComponentProps } from 'vue-component-type-helpers'
import type { VisualizationIdentifier, VisualizationMetadata } from 'ydoc-shared/yjsModel'

Expand All @@ -22,26 +22,30 @@ interface Emit {
interface NodeVisualizationOptions {
vis: ToValue<Opt<VisualizationMetadata>>
nodeHovered: ToValue<boolean>
nodeRect: ToValue<Rect>
nodeWidgetsSize: ToValue<Vec2>
nodePos: ToValue<Vec2>
scale: ToValue<number>
isFocused: ToValue<boolean>
typeinfo: ToValue<Opt<TypeInfo>>
dataSource: ToValue<Opt<VisualizationDataSource | RawDataSource>>
hidden: ToValue<boolean>
emit: Emit
onResize?: (rect0: Rect, rect1: Rect) => void
}

/** Composable managing the state of the visualization for a node. */
export function useNodeVisualization({
vis,
nodeHovered,
nodeRect,
nodeWidgetsSize,
nodePos,
scale,
isFocused,
typeinfo,
dataSource,
hidden,
emit,
onResize,
}: NodeVisualizationOptions) {
const keyboard = injectBubblingKeyboard()
const metadata = computed(() => toValue(vis))
Expand Down Expand Up @@ -91,24 +95,37 @@ export function useNodeVisualization({
if (!visible && visualizationHovered.value) visualizationHovered.value = false
})

const visSize = shallowRef<Vec2>()
const visibleVisRect = computed((): Opt<Rect> => {
if (!isVisualizationVisible.value || toValue(hidden) || !visSize.value) return null
const nodeRectValue = toValue(nodeRect)
return new Rect(
nodeRectValue.pos,
new Vec2(visSize.value.x, nodeRectValue.size.y + visSize.value.y),
)
const effectiveHeight = ref<number>()
const visibleVisHeight = computed((): number => {
if (!isVisualizationVisible.value || toValue(hidden)) return 0
return effectiveHeight.value ?? 0
})
const effectiveWidth = ref<number>()
const visibleVisWidth = computed((): number => {
if (!isVisualizationVisible.value || toValue(hidden)) return 0
return effectiveWidth.value ?? 0
})
const visibleSize = computed<Vec2 | undefined>((prev) => {
if (effectiveHeight.value == null || effectiveWidth.value == null) return
const size = new Vec2(visibleVisWidth.value, visibleVisHeight.value)
return prev?.equals(size) ? prev : size
})
watch(visibleSize, (size1, size0) => {
if (!size1 || !size0 || size1.equals(size0)) return
const widgetsHeightVec = new Vec2(0, toValue(nodeWidgetsSize).y)
const pos = toValue(nodePos)
const rect0 = new Rect(pos, size0.add(widgetsHeightVec))
const rect1 = new Rect(pos, size1.add(widgetsHeightVec))
onResize?.(rect0, rect1)
})

const visualization = computed((): ComponentProps<typeof GraphVisualization> => {
const { size: nodeSize, pos: nodePosition } = toValue(nodeRect)
return {
show: isVisualizationVisible.value,
width: visualizationWidth.value,
nodeSize,
nodeSize: toValue(nodeWidgetsSize),
scale: toValue(scale),
nodePosition,
nodePosition: toValue(nodePos),
currentType: metadata.value?.identifier,
dataSource: toValue(dataSource) ?? undefined,
typeinfo: toValue(typeinfo) ?? undefined,
Expand All @@ -118,7 +135,8 @@ export function useNodeVisualization({
isFullscreenAllowed: true,
isResizable: true,
'onUpdate:hovered': (event) => (visualizationHovered.value = event),
'onUpdate:effectiveSize': (event) => (visSize.value = event),
'onUpdate:effectiveHeight': (event) => (effectiveHeight.value = event),
'onUpdate:effectiveWidth': (event) => (effectiveWidth.value = event),
'onUpdate:id': (event) => emit('update:visualizationId', event),
'onUpdate:enabled': (event) => emit('update:visualizationEnabled', event),
'onUpdate:height': (event) => emit('update:visualizationHeight', event),
Expand All @@ -127,10 +145,10 @@ export function useNodeVisualization({
})

return {
visualizationWidth,
visualizationWidth: visibleVisWidth,
isVisualizationEnabled,
isVisualizationPreviewed,
visRect: visibleVisRect,
vizHeight: visibleVisHeight,
visualization,
}
}
Loading