Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve dropdown arrows placement #9909

Merged
merged 12 commits into from
May 10, 2024
Merged

Conversation

vitvakatu
Copy link
Contributor

Pull Request Description

Closes #9428

Hilariously tricky thing to implement. Besides placing arrows below constructor names, we also position them under values in argument placeholders.

We also added a new feature to the widget tree: the allowAsLeaf property. See provided documentation.

working.arrow.placement.mp4

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    TypeScript,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • Unit tests have been written where possible.

@vitvakatu vitvakatu added the -gui label May 9, 2024
@vitvakatu vitvakatu self-assigned this May 9, 2024
Comment on lines +209 to +216
// Find the top-most PropertyAccess, and return its rhs id.
// It will be used to place the dropdown arrow under the constructor name.
let node = props.input.value
while (node instanceof Ast.Ast) {
if (node instanceof Ast.PropertyAccess) return node.rhs.id
if (node instanceof Ast.App) node = node.function
else break
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes constructors have arguments, but call resolution happens only in WidgetFunction, which has a lower priority than WidgetSelection. I think this code will cover the majority of cases when we have a constructor call, but perhaps there are some common cases that I missed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes constructors have arguments, but call resolution happens only in WidgetFunction, which has a lower priority than WidgetSelection. I think this code will cover the majority of cases when we have a constructor call, but perhaps there are some common cases that I missed.

It will also catch all normal function calls (for example, in node opreator1.select_columns [operator2.first] the arrow will be under first - I'm not sure if it's expected. On the other hand, it's rather an advanced usage).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that’s the case. I don’t think we can solve it without expensive operations to determine if we have a constructor call or a function call. Perhaps we can analyze each node on a higher level, preparing all the necessary information in advance and only fetching it in WidgetFunction, but for now I suggest to leave it as it is.

Comment on lines +381 to +384
<Teleport v-if="arrowLocation" :to="arrowLocation">
<SvgIcon v-if="isHovered" name="arrow_right_head_only" class="arrow" />
</Teleport>
<SvgIcon v-else-if="isHovered" name="arrow_right_head_only" class="arrow" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may wonder why to duplicate instead of using <Teleport :disabled='...'>. The reason is that :to should point at a valid mounted reference at the time of its mounting. Here, however, the destination would only be available after mount. In other words, you can’t have <Teleport :disabled="bla" :to="undefined">, Vue will bail out. I choose not to state it in the code, as the comment will be copied to application html tree, which I find ugly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I choose not to state it in the code, as the comment will be copied to application html tree, which I find ugly.

In production builds, the comments are removed: https://vuejs.org/api/application.html#app-config-compileroptions-comments If you want, you may set the config to remove also in dev builds, but perhaps comments may be useful there?

In either case, please put here a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good to know, added a comment there.

@@ -205,6 +205,14 @@ export interface WidgetOptions<T extends WidgetInput> {
// A list of widget kinds that will be prevented from being used on the same node as this widget,
// once this widget is used.
prevent?: WidgetComponent<any>[]
/** Allow to select this widget as the leaf of the widget tree for particular widget input.
* `true` by default. The widget marked with `false` *must* have a child,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second sentence is kinda obvious, but I still want to write it down just in case.

@vitvakatu vitvakatu marked this pull request as ready for review May 9, 2024 13:40
@vitvakatu vitvakatu added the CI: No changelog needed Do not require a changelog entry for this PR. label May 9, 2024
Copy link
Contributor

@farmaazon farmaazon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the implementation, but I think the docs/names should be improved.

Comment on lines +209 to +216
// Find the top-most PropertyAccess, and return its rhs id.
// It will be used to place the dropdown arrow under the constructor name.
let node = props.input.value
while (node instanceof Ast.Ast) {
if (node instanceof Ast.PropertyAccess) return node.rhs.id
if (node instanceof Ast.App) node = node.function
else break
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes constructors have arguments, but call resolution happens only in WidgetFunction, which has a lower priority than WidgetSelection. I think this code will cover the majority of cases when we have a constructor call, but perhaps there are some common cases that I missed.

It will also catch all normal function calls (for example, in node opreator1.select_columns [operator2.first] the arrow will be under first - I'm not sure if it's expected. On the other hand, it's rather an advanced usage).

Comment on lines +381 to +384
<Teleport v-if="arrowLocation" :to="arrowLocation">
<SvgIcon v-if="isHovered" name="arrow_right_head_only" class="arrow" />
</Teleport>
<SvgIcon v-else-if="isHovered" name="arrow_right_head_only" class="arrow" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I choose not to state it in the code, as the comment will be copied to application html tree, which I find ugly.

In production builds, the comments are removed: https://vuejs.org/api/application.html#app-config-compileroptions-comments If you want, you may set the config to remove also in dev builds, but perhaps comments may be useful there?

In either case, please put here a comment.


<template>
<div ref="teleportTarget" class="WidgetSelectionArrow">
<NodeWidget :input="innerInput" allowEmpty />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allowEmpty here, while also not allowing as leaf?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, an artifact from previous implementation

app/gui2/src/providers/__tests__/widgetRegistry.test.ts Outdated Show resolved Hide resolved
Comment on lines 208 to 209
/** Allow to select this widget as the leaf of the widget tree for particular widget input.
* `true` by default. The widget marked with `false` *must* have a child,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the terminology is not quite right here: in my understanding of child term, a widget could actually have children, but still be discarded when allowAsLeaf is false.

The best example is WidgetHierarchy - usually picked as last resort for AST tree. This tree may still have children, but those will make different input for widgets, so the Hierarchy's input will match no other widget - making it being discarded when allowAsLeaf will be false.

I think this could be renamed to wrapperOnly or something like that, and the meaning is "if true, this widget will be matched only when at least one another widget is guaranteed to be matched for the same input". Or better :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your understanding is correct, and so is my comment – the important words of the first sentence are for particular widget input. I don’t think wrapperOnly and inversion of boolean have a strong preference here, so I left it as it is. wrapperOnly has the same problem – if widget is wrapperOnly, it still can be selected as a non-wrapper if it has no children with the same input. However, I liked your explanation if true, this widget will be matched only when at least one another widget is guaranteed to be matched for the same input; and reused it. (inversing the boolean)

app/gui2/src/providers/widgetRegistry.ts Outdated Show resolved Hide resolved
Comment on lines 397 to 400
// If we found a perfect match, we can return immediately, as there can be no better match.
// If we found a leaf match, we can safely return – there will be another leaf later down the tree.
// One caveat here is that if the widget marked with `allowAsLeaf = false` has no children. Then we
// can pick it here, and it will effectively become leaf.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would somehow merge the first two sentences. And I have an impression, that the caveat does not explain too much - after all, the allowAsLeaf is about guarantees, the widget may do whatever it wants.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think merging is possible without murdering the grammar, but I removed the part about caveat and rephrased it.

@vitvakatu vitvakatu added the CI: Ready to merge This PR is eligible for automatic merge label May 10, 2024
@mergify mergify bot merged commit 4376a5a into develop May 10, 2024
37 checks passed
@mergify mergify bot deleted the wip/vitvakatu/arrows-positions branch May 10, 2024 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
-gui CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

The dropdown triangle for constructors with arguments should be under the constructor name not centered.
2 participants