package mindustry.logic; import arc.*; import arc.func.*; import arc.graphics.*; import arc.graphics.g2d.*; import arc.input.*; import arc.math.*; import arc.math.geom.*; import arc.scene.*; import arc.scene.event.*; import arc.scene.ui.*; import arc.scene.ui.layout.*; import arc.struct.*; import arc.util.*; import mindustry.*; import mindustry.gen.*; import mindustry.graphics.*; import mindustry.logic.LStatements.*; import mindustry.ui.*; public class LCanvas extends Table{ private static final Seq tmpOccupiers1 = new Seq<>(); private static final Seq tmpOccupiers2 = new Seq<>(); private static final Bits tmpBits1 = new Bits(); private static final Bits tmpBits2 = new Bits(); private static final int invalidJump = Integer.MAX_VALUE; // terrible hack //ew static variables static LCanvas canvas; private static final boolean dynamicJumpHeights = true; public DragLayout statements; public ScrollPane pane; StatementElem dragging; StatementElem hovered; float targetWidth; boolean privileged; Seq tooltips = new Seq<>(); public LCanvas(){ canvas = this; Core.scene.addListener(new InputListener(){ @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ //hide tooltips on tap for(var t : tooltips){ t.container.toFront(); } Core.app.post(() -> { tooltips.each(Tooltip::hide); tooltips.clear(); }); return super.touchDown(event, x, y, pointer, button); } }); rebuild(); } /** @return if statement elements should have rows. */ public static boolean useRows(){ return Core.graphics.getWidth() < Scl.scl(900f) * 1.2f; } public static void tooltip(Cell cell, String key){ String lkey = key.toLowerCase().replace(" ", ""); if(Core.settings.getBool("logichints", true) && Core.bundle.has(lkey)){ var tip = new Tooltip(t -> t.background(Styles.black8).margin(4f).add("[lightgray]" + Core.bundle.get(lkey)).style(Styles.outlineLabel)); //mobile devices need long-press tooltips if(Vars.mobile){ cell.get().addListener(new ElementGestureListener(20, 0.4f, 0.43f, 0.15f){ @Override public boolean longPress(Element element, float x, float y){ tip.show(element, x, y); canvas.tooltips.add(tip); //prevent touch down for other listeners for(var list : cell.get().getListeners()){ if(list instanceof ClickListener cl){ cl.cancel(); } } return true; } }); }else{ cell.get().addListener(tip); } } } public static void tooltip(Cell cell, Enum key){ String cl = key.getClass().getSimpleName().toLowerCase() + "." + key.name().toLowerCase(); if(Core.bundle.has(cl)){ tooltip(cell, cl); }else{ tooltip(cell, "lenum." + key.name()); } } public void rebuild(){ targetWidth = useRows() ? 400f : 900f; float s = pane != null ? pane.getVisualScrollY() : 0f; String toLoad = statements != null ? save() : null; clear(); statements = new DragLayout(); pane = pane(t -> { t.center(); t.add(statements).pad(2f).center().width(targetWidth); t.addChild(statements.jumps); statements.jumps.touchable = Touchable.disabled; statements.jumps.update(() -> statements.jumps.setCullingArea(t.getCullingArea())); statements.jumps.cullable = false; }).grow().get(); pane.setFlickScroll(false); pane.setScrollYForce(s); if(toLoad != null){ load(toLoad); } } public void add(LStatement statement){ statements.addChild(new StatementElem(statement)); } public void addAt(int at, LStatement statement){ statements.addChildAt(at, new StatementElem(statement)); } public String save(){ Seq st = statements.getChildren().as().map(s -> s.st); st.each(LStatement::saveUI); return LAssembler.write(st); } public void load(String asm){ statements.jumps.clear(); Seq statements = LAssembler.read(asm, privileged); statements.truncate(LExecutor.maxInstructions); this.statements.clearChildren(); for(LStatement st : statements){ add(st); } for(LStatement st : statements){ st.setupUI(); } this.statements.updateJumpHeights = true; } public void clearStatements(){ statements.jumps.clear(); statements.clearChildren(); } StatementElem checkHovered(){ Element e = Core.scene.getHoverElement(); if(e != null){ while(e != null && !(e instanceof StatementElem)){ e = e.parent; } } if(e == null || isDescendantOf(e)) return null; return (StatementElem)e; } @Override public void act(float delta){ super.act(delta); hovered = checkHovered(); if(Core.input.isTouched()){ float y = Core.input.mouseY(); float dst = Math.min(y - this.y, Core.graphics.getHeight() - y); if(dst < Scl.scl(100f)){ //scroll margin int sign = Mathf.sign(Core.graphics.getHeight()/2f - y); pane.setScrollY(pane.getScrollY() + sign * Scl.scl(15f) * Time.delta); } } } public class DragLayout extends WidgetGroup{ float space = Scl.scl(10f), prefWidth, prefHeight; Seq seq = new Seq<>(); int insertPosition = 0; boolean invalidated; public Group jumps = new WidgetGroup(); private Seq processedJumps = new Seq<>(); private IntMap reprBefore = new IntMap<>(); private IntMap reprAfter = new IntMap<>(); public boolean updateJumpHeights = true; { setTransform(true); } @Override public void layout(){ invalidated = true; float cy = 0; seq.clear(); float totalHeight = getChildren().sumf(e -> e.getPrefHeight() + space); if(height != totalHeight || width != Scl.scl(targetWidth)){ height = prefHeight = totalHeight; width = prefWidth = Scl.scl(targetWidth); invalidateHierarchy(); } //layout everything normally for(int i = 0; i < getChildren().size; i++){ Element e = getChildren().get(i); //ignore the dragged element if(dragging == e) continue; e.setSize(width, e.getPrefHeight()); e.setPosition(0, totalHeight - cy, Align.topLeft); ((StatementElem)e).updateAddress(i); cy += e.getPrefHeight() + space; seq.add(e); } //insert the dragged element if necessary if(dragging != null){ //find real position of dragged element top float realY = dragging.getY(Align.top) + dragging.translation.y; insertPosition = 0; for(int i = 0; i < seq.size; i++){ Element cur = seq.get(i); //find fit point if(realY < cur.y && (i == seq.size - 1 || realY > seq.get(i + 1).y)){ insertPosition = i + 1; break; } } float shiftAmount = dragging.getHeight() + space; //shift elements below insertion point down for(int i = insertPosition; i < seq.size; i++){ seq.get(i).y -= shiftAmount; } } if(dynamicJumpHeights){ if(updateJumpHeights) setJumpHeights(); updateJumpHeights = false; } if(parent != null && parent instanceof Table){ setCullingArea(parent.getCullingArea()); } } private void setJumpHeights(){ SnapshotSeq jumpsChildren = jumps.getChildren(); processedJumps.clear(); reprBefore.clear(); reprAfter.clear(); jumpsChildren.each(e -> { if(!(e instanceof JumpCurve e2)) return; e2.prepareHeight(); if(e2.jumpUIBegin == invalidJump) return; if(e2.flipped){ JumpCurve prev = reprAfter.get(e2.jumpUIBegin); if(prev != null && prev.jumpUIEnd >= e2.jumpUIEnd) return; reprAfter.put(e2.jumpUIBegin, e2); }else{ JumpCurve prev = reprBefore.get(e2.jumpUIEnd); if(prev != null && prev.jumpUIBegin <= e2.jumpUIBegin) return; reprBefore.put(e2.jumpUIEnd, e2); } }); processedJumps.add(reprBefore.values().toArray()); processedJumps.add(reprAfter.values().toArray()); processedJumps.sort((a, b) -> a.jumpUIBegin - b.jumpUIBegin); Seq occupiers = tmpOccupiers1; Bits occupied = tmpBits1; occupiers.clear(); occupied.clear(); for(int i = 0; i < processedJumps.size; i++){ JumpCurve cur = processedJumps.get(i); occupiers.retainAll(e -> { if(e.jumpUIEnd > cur.jumpUIBegin) return true; occupied.clear(e.predHeight); return false; }); int h = getJumpHeight(i, occupiers, occupied); occupiers.add(cur); occupied.set(h); } occupiers.clear(); jumpsChildren.each(e -> { if(!(e instanceof JumpCurve e2)) return; if(e2.jumpUIBegin == invalidJump) return; e2.predHeight = e2.flipped ? reprAfter.get(e2.jumpUIBegin).predHeight : reprBefore.get(e2.jumpUIEnd).predHeight; e2.markedDone = true; }); } private int getJumpHeight(int index, Seq occupiers, Bits occupied){ JumpCurve jmp = processedJumps.get(index); if(jmp.markedDone) return jmp.predHeight; Seq tmpOccupiers = tmpOccupiers2; Bits tmpOccupied = tmpBits2; tmpOccupiers.set(occupiers); tmpOccupied.set(occupied); int max = -1; for(int i = index + 1; i < processedJumps.size; i++){ JumpCurve cur = processedJumps.get(i); if(cur.jumpUIEnd > jmp.jumpUIEnd) continue; tmpOccupiers.retainAll(e -> { if(e.jumpUIEnd > cur.jumpUIBegin) return true; tmpOccupied.clear(e.predHeight); return false; }); int h = getJumpHeight(i, tmpOccupiers, tmpOccupied); tmpOccupiers.add(cur); tmpOccupied.set(h); max = Math.max(max, h); } jmp.predHeight = occupied.nextClearBit(max + 1); jmp.markedDone = true; tmpOccupiers.clear(); return jmp.predHeight; } @Override public float getPrefWidth(){ return prefWidth; } @Override public float getPrefHeight(){ return prefHeight; } @Override public void draw(){ Draw.alpha(parentAlpha); //draw selection box indicating placement position if(dragging != null && insertPosition <= seq.size){ float shiftAmount = dragging.getHeight(); float lastX = x; float lastY = insertPosition == 0 ? height + y : seq.get(insertPosition - 1).y + y - space; Tex.pane.draw(lastX, lastY - shiftAmount, width, dragging.getHeight()); } if(invalidated){ children.each(c -> c.cullable = false); } super.draw(); if(invalidated){ children.each(c -> c.cullable = true); invalidated = false; } } void finishLayout(){ if(dragging != null){ //reset translation first for(Element child : getChildren()){ child.setTranslation(0, 0); } clearChildren(); //reorder things for(int i = 0; i <= insertPosition - 1 && i < seq.size; i++){ addChild(seq.get(i)); } addChild(dragging); for(int i = insertPosition; i < seq.size; i++){ addChild(seq.get(i)); } dragging = null; invalidateHierarchy(); } updateJumpHeights = true; } } public class StatementElem extends Table{ public LStatement st; public int index; Label addressLabel; public StatementElem(LStatement st){ this.st = st; st.elem = this; background(Tex.whitePane); setColor(st.category().color); margin(0f); touchable = Touchable.enabled; table(Tex.whiteui, t -> { t.color.set(color); t.addListener(new HandCursorListener()); t.margin(6f); t.touchable = Touchable.enabled; t.add(st.name()).style(Styles.outlineLabel).name("statement-name").color(color).padRight(8); t.add().growX(); addressLabel = t.add(index + "").style(Styles.outlineLabel).color(color).padRight(8).get(); //taken from foo's client t.button(Icon.add, Styles.logici, () -> Vars.ui.logic.showAddDialog(index + 1)) .disabled(b -> canvas.statements.getChildren().size >= LExecutor.maxInstructions).size(24f).padRight(6); t.button(Icon.copy, Styles.logici, () -> { }).size(24f).padRight(6).get().tapped(this::copy); t.button(Icon.cancel, Styles.logici, () -> { remove(); dragging = null; statements.updateJumpHeights = true; }).size(24f); t.addListener(new InputListener(){ float lastx, lasty; @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){ //don't start dragging when pressing the menu buttons if(event.targetActor instanceof Image) return false; if(button == KeyCode.mouseMiddle){ copy(); return false; } Vec2 v = localToParentCoordinates(Tmp.v1.set(x, y)); lastx = v.x; lasty = v.y; dragging = StatementElem.this; toFront(); statements.updateJumpHeights = true; statements.invalidate(); return true; } @Override public void touchDragged(InputEvent event, float x, float y, int pointer){ Vec2 v = localToParentCoordinates(Tmp.v1.set(x, y)); translation.add(v.x - lastx, v.y - lasty); lastx = v.x; lasty = v.y; statements.invalidate(); } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode button){ statements.finishLayout(); } }); }).growX().height(38); row(); table(t -> { t.left(); t.marginLeft(4); t.setColor(color); st.build(t); }).pad(4).padTop(2).left().grow(); marginBottom(7); } public void updateAddress(int index){ this.index = index; addressLabel.setText(index + ""); } public void copy(){ st.saveUI(); LStatement copy = st.copy(); if(copy instanceof JumpStatement st && st.destIndex != -1){ int index = statements.getChildren().indexOf(this); if(index != -1 && index < st.destIndex){ st.destIndex ++; } } if(copy != null){ StatementElem s = new StatementElem(copy); statements.addChildAfter(StatementElem.this, s); copy.elem = s; copy.setupUI(); statements.updateJumpHeights = true; } } @Override public void draw(){ float pad = 5f; Fill.dropShadow(x + width/2f, y + height/2f, width + pad, height + pad, 10f, 0.9f * parentAlpha); Draw.color(0, 0, 0, 0.3f * parentAlpha); Fill.crect(x, y, width, height); Draw.reset(); super.draw(); } } public static class JumpButton extends ImageButton{ Color hoverColor = Pal.place; Prov to; boolean selecting; float mx, my; ClickListener listener; public JumpCurve curve; public StatementElem elem; public JumpButton(Prov getter, Cons setter, StatementElem elem){ super(Tex.logicNode, new ImageButtonStyle(){{ imageUpColor = Color.white; }}); this.elem = elem; to = getter; addListener(listener = new ClickListener()); addListener(new InputListener(){ @Override public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode code){ selecting = true; setter.get(null); mx = x; my = y; canvas.statements.updateJumpHeights = true; return true; } @Override public void touchDragged(InputEvent event, float x, float y, int pointer){ mx = x; my = y; } @Override public void touchUp(InputEvent event, float x, float y, int pointer, KeyCode code){ localToStageCoordinates(Tmp.v1.set(x, y)); StatementElem elem = canvas.hovered; if(elem != null && !isDescendantOf(elem)){ setter.get(elem); }else{ setter.get(null); } selecting = false; canvas.statements.updateJumpHeights = true; } }); update(() -> { if(to.get() != null && to.get().parent == null){ setter.get(null); } setColor(listener.isOver() ? hoverColor : Color.white); getStyle().imageUpColor = this.color; }); curve = new JumpCurve(this); } @Override protected void setScene(Scene stage){ super.setScene(stage); if(stage == null){ curve.remove(); }else{ canvas.statements.jumps.addChild(curve); } } } public static class JumpCurve extends Element{ public JumpButton button; private boolean invertedHeight; // for jump prediction; see DragLayout public int predHeight = 0; public boolean markedDone = false; public int jumpUIBegin = 0, jumpUIEnd = 0; public boolean flipped = false; private float uiHeight = 60f; public JumpCurve(JumpButton button){ this.button = button; } @Override public void setSize(float width, float height){ if(height < 0){ y += height; height = -height; invertedHeight = true; } super.setSize(width, height); } @Override public void act(float delta){ super.act(delta); //MDTX(WayZer, 2024/8/6) Support Cull invertedHeight = false; Group desc = canvas.statements.jumps.parent; Vec2 t = Tmp.v1.set(button.getWidth() / 2f, button.getHeight() / 2f); button.localToAscendantCoordinates(desc, t); setPosition(t.x, t.y); Element hover = button.to.get() == null && button.selecting ? canvas.hovered : button.to.get(); if(hover != null){ t.set(hover.getWidth(), hover.getHeight() / 2f); hover.localToAscendantCoordinates(desc, t); setSize(t.x - x, t.y - y); }else if(button.selecting){ setSize(button.mx, button.my); }else{ setSize(0, 0); } if(button.listener.isOver()){ toFront(); } } @Override public void draw(){ if(height == 0) return; Vec2 t = Tmp.v1.set(width, !invertedHeight ? height : 0), r = Tmp.v2.set(0, !invertedHeight ? 0 : height); Group desc = canvas.pane; localToAscendantCoordinates(desc, r); localToAscendantCoordinates(desc, t); drawCurve(r.x, r.y, t.x, t.y); float s = button.getWidth(); Draw.color(button.color, parentAlpha); Tex.logicNode.draw(t.x + s * 0.75f, t.y - s / 2f, -s, s); Draw.reset(); } public void drawCurve(float x, float y, float x2, float y2){ Lines.stroke(Scl.scl(4f), button.color); Draw.alpha(parentAlpha); // exponential smoothing uiHeight = Mathf.lerp( Scl.scl(Core.graphics.isPortrait() ? 20f : 40f) + Scl.scl(Core.graphics.isPortrait() ? 8f : 10f) * (float) predHeight, uiHeight, dynamicJumpHeights ? Mathf.pow(0.9f, Time.delta) : 0 ); //trapezoidal jumps float dy = (y2 == y ? 0f : y2 > y ? 1f : -1f) * uiHeight * 0.5f; //there's absolutely a better way to detect invalid trapezoids, but this probably isn't *that* slow and I don't care to fix it right now if(Intersector.intersectSegments(x, y, x + uiHeight, y + dy, x2, y2, x + uiHeight, y2 - dy, Tmp.v3)){ Lines.beginLine(); Lines.linePoint(x, y); Lines.linePoint(Tmp.v3.x, Tmp.v3.y); Lines.linePoint(x2, y2); Lines.endLine(); }else{ Lines.beginLine(); Lines.linePoint(x, y); Lines.linePoint(x + uiHeight, y + dy); Lines.linePoint(x + uiHeight, y2 - dy); Lines.linePoint(x2, y2); Lines.endLine(); } } public void prepareHeight(){ if(this.button.to.get() == null){ this.markedDone = true; this.predHeight = 0; this.flipped = false; this.jumpUIBegin = this.jumpUIEnd = invalidJump; }else{ this.markedDone = false; int i = this.button.elem.index; int j = this.button.to.get().index; this.flipped = i >= j; this.jumpUIBegin = Math.min(i,j); this.jumpUIEnd = Math.max(i,j); // height will be recalculated later } } } }