package nl.utwente.viskell.ui.components; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.fxml.FXML; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Pane; import nl.utwente.viskell.haskell.expr.*; import nl.utwente.viskell.haskell.type.*; import nl.utwente.viskell.ui.BlockContainer; import nl.utwente.viskell.ui.ComponentLoader; import nl.utwente.viskell.ui.TouchContext; /** Represent a lambda construct with internal anchor and blocks */ public class LambdaContainer extends BorderPane implements ComponentLoader, WrappedContainer { /* area containing argument anchors */ @FXML private Pane argSpace; /* area contain result anchor */ @FXML private Pane resSpace; /* The block this lambda is contained by. */ private LambdaBlock wrapper; /* The lambda argument anchors */ private List<BinderAnchor> args; /** The result anchor */ private ResultAnchor res; /** Whether the anchor types are fresh*/ private boolean freshAnchorTypes; /** Status of change updating process in this block. */ private boolean updateInProgress; /** A set of blocks that belong to this container */ protected Set<Block> attachedBlocks; /** * Constructs a LambdaContainer for an untyped lambda of n arguments. * @param arity the number of arguments of this lambda. */ public LambdaContainer(LambdaBlock wrapper, int arity) { super(); this.loadFXML("LambdaContainer"); this.wrapper = wrapper; attachedBlocks = new HashSet<>(); this.args = new ArrayList<>(); for (int i = 0; i < arity; i++) { this.args.add(new BinderAnchor(this, wrapper, new Binder("a_" + i))); } this.res = new ResultAnchor(this, wrapper, Optional.empty()); this.argSpace.getChildren().addAll(this.args); this.resSpace.getChildren().add(this.res); TouchContext context = new TouchContext(this, false); context.setPanningAction((deltaX, deltaY) -> { if (! this.wrapper.isActivated()) { this.wrapper.relocate(this.wrapper.getLayoutX() + deltaX, this.wrapper.getLayoutY() + deltaY); } }); } /** * Set an explicit type requirement on this LambdaContainer. * @param signature the full function type. */ protected void enforceExplicitType(Type signature) { Type constraint = signature.getFresh(); constraint.enforcePolymorphism(); Type t = constraint; for (BinderAnchor arg : this.args) { FunType ft = (FunType) t; arg.setExactRequiredType(ft.getArgument()); t = ft.getResult(); } this.res.setConstraintType(t); } public int argCount() { return this.args.size(); } /** Adds extra input binder anchor to this lambda */ public void addExtraInput() { BinderAnchor arg = new BinderAnchor(this, wrapper, new Binder("a_" + this.args.size())); this.args.add(arg); this.argSpace.getChildren().add(arg); this.wrapper.initiateConnectionChanges(); } /** Removes the last input binder anchor of this lambda */ public void removeLastInput() { if (!this.args.isEmpty()) { BinderAnchor arg = this.args.remove(this.args.size()-1); arg.removeConnections(); this.argSpace.getChildren().remove(arg); this.wrapper.initiateConnectionChanges(); } } @Override public void refreshAnchorTypes() { if (this.updateInProgress || this.freshAnchorTypes) { return; // refresh anchor types only once } this.freshAnchorTypes = true; TypeScope scope = new TypeScope(); for (BinderAnchor arg : this.args) { arg.refreshType(scope); } this.res.refreshAnchorType(scope); } /** returns the current (inferred) type of this lambda */ protected Type getLambdaType() { ArrayList<Type> types = new ArrayList<>(); for (BinderAnchor arg : this.args) { types.add(arg.getType(Optional.empty())); } types.add(this.res.getType()); return Type.fun(types.toArray(new Type[this.args.size()+1])); } @Override public final void handleConnectionChanges(boolean finalPhase) { if (this.updateInProgress != finalPhase) { return; // avoid doing extra work and infinite recursion } if (! finalPhase) { // in first phase ensure that anchor types are refreshed this.refreshAnchorTypes(); } this.updateInProgress = !finalPhase; this.freshAnchorTypes = false; // first propagate up from the result anchor this.res.getConnection().ifPresent(c -> c.handleConnectionChangesUpwards(finalPhase)); // also propagate in from above in case the lambda is partially connected for (BinderAnchor arg : this.args) { for (InputAnchor anchor : arg.getOppositeAnchors()) { anchor.handleConnectionChanges(finalPhase); // take the type of argument connections in account even if the connected block is being processed anchor.getConnection().ifPresent(c -> c.handleConnectionChangesUpwards(finalPhase)); } } // propagate internal type changes outwards this.wrapper.handleConnectionChanges(finalPhase); } /** @return The local expression this LambdaContainer represents. */ public Expression getLocalExpr(Set<OutputAnchor> outsideAnchors) { Set<OutputAnchor> escapingAnchors = new HashSet<OutputAnchor>(); List<Binder> binders = args.stream().map(arg -> arg.binder).collect(Collectors.toList()); LetExpression body = new LetExpression(res.getLocalExpr(escapingAnchors), false); res.extendExprGraph(body, this, escapingAnchors); /** * Iterate over the container until it doesn't find new nodes. */ boolean cont = true; for (int numAnchors = -1; numAnchors != escapingAnchors.size() || cont; numAnchors = escapingAnchors.size()) { for (OutputAnchor anchor : new ArrayList<>(escapingAnchors)) { if (anchor.getContainer() == this) { anchor.extendExprGraph(body, this, escapingAnchors); } else { outsideAnchors.add(anchor); } } cont = (numAnchors != escapingAnchors.size()); } return new Lambda(binders, body); } /** Called when the VisualState changed. */ public void invalidateVisualState() { for (BinderAnchor arg : this.args) { arg.invalidateVisualState(); } this.res.invalidateVisualState(); } @Override public LambdaBlock getWrapper() { return this.wrapper; } public List<ConnectionAnchor> getAllAnchors() { List<ConnectionAnchor> result = new ArrayList<>(this.args); result.add(this.res); return result; } @Override public void attachBlock(Block block) { attachedBlocks.add(block); } @Override public void detachBlock(Block block) { attachedBlocks.remove(block); } @Override public Stream<Block> getAttachedBlocks() { return this.attachedBlocks.stream(); } @Override public BlockContainer getParentContainer() { return wrapper.getContainer(); } @Override public Node asNode() { return this; } @Override public void deleteAllLinks() { this.args.forEach(OutputAnchor::removeConnections); this.res.removeConnections(); new ArrayList<>(this.attachedBlocks).forEach(block -> block.moveIntoContainer(this.getParentContainer())); } @Override public Bounds containmentBoundsInScene() { Bounds local = this.getBoundsInLocal(); // include top and bottom border of the DefinitionBlock BoundingBox withBorders = new BoundingBox(local.getMinX(), local.getMinY()-25, local.getWidth(), local.getHeight()+50); return this.localToScene(withBorders); } @Override public void expandToFit(Bounds blockBounds) { Bounds containerBounds = this.wrapper.getToplevel().sceneToLocal(this.getCenter().localToScene(this.getCenter().getBoundsInLocal())); double shiftX = Math.min(0, blockBounds.getMinX() - containerBounds.getMinX()); double shiftY = Math.min(0, blockBounds.getMinY() - containerBounds.getMinY()); double extraX = Math.max(0, blockBounds.getMaxX() - containerBounds.getMaxX()) + Math.abs(shiftX); double extraY = Math.max(0, blockBounds.getMaxY() - containerBounds.getMaxY()) + Math.abs(shiftY); this.wrapper.shiftAndGrow(shiftX, shiftY, extraX, extraY); // also resize its parent in case of nested containers Bounds fitBounds = this.wrapper.getBoundsInParent(); this.getParentContainer().expandToFit(new BoundingBox(fitBounds.getMinX()-10, fitBounds.getMinY()-10, fitBounds.getWidth()+20, fitBounds.getHeight()+20)); } }