From dfd2654c87c4d70018ce866d9c17f9de01e2b530 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 23:52:06 +0200 Subject: [PATCH 1/2] gui: KeYther, a Swing GUI for the Rusty prover Port the Solidity KeYther GUI to a new keyext.rusty.gui module (proof tree, sequent editor with interactive taclet application and a side-effect-free result preview, strategy and preferences). Exposes printSequent and adds getResultSequentChanges for the preview. --- keyext.rusty.gui/build.gradle | 34 ++ .../org/key_project/rusty/gui/EditorArea.java | 283 +++++++++++ .../org/key_project/rusty/gui/GoalsView.java | 92 ++++ .../org/key_project/rusty/gui/MainWindow.java | 465 ++++++++++++++++++ .../key_project/rusty/gui/NodeInfoView.java | 168 +++++++ .../rusty/gui/PreferencesDialog.java | 192 ++++++++ .../key_project/rusty/gui/ProofContext.java | 76 +++ .../key_project/rusty/gui/ProofTreePanel.java | 451 +++++++++++++++++ .../org/key_project/rusty/gui/RustyMain.java | 34 ++ .../key_project/rusty/gui/SequentView.java | 443 +++++++++++++++++ .../key_project/rusty/gui/StrategyView.java | 250 ++++++++++ .../rusty/gui/TacletCompletionDialog.java | 382 ++++++++++++++ .../java/org/key_project/rusty/gui/Theme.java | 59 +++ .../rusty/gui/icons/autoModeStart.png | Bin 0 -> 8064 bytes .../rusty/gui/icons/key-color-icon-square.png | Bin 0 -> 8276 bytes .../org/key_project/rusty/gui/icons/open.png | Bin 0 -> 14175 bytes .../rusty/gui/icons/openMostRecent.png | Bin 0 -> 27595 bytes .../rusty/gui/icons/pruneProof.png | Bin 0 -> 29042 bytes .../key_project/rusty/gui/icons/saveFile.png | Bin 0 -> 26285 bytes .../proof/io/OutputStreamProofSaver.java | 2 +- .../rusty/rule/TacletExecutor.java | 9 + .../executor/rustydl/FindTacletExecutor.java | 30 ++ .../rustydl/NoFindTacletExecutor.java | 23 + settings.gradle | 1 + 24 files changed, 2993 insertions(+), 1 deletion(-) create mode 100644 keyext.rusty.gui/build.gradle create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/EditorArea.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/GoalsView.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/MainWindow.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/NodeInfoView.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/PreferencesDialog.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofContext.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofTreePanel.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/RustyMain.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/SequentView.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/StrategyView.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/TacletCompletionDialog.java create mode 100644 keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/Theme.java create mode 100644 keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/autoModeStart.png create mode 100644 keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/key-color-icon-square.png create mode 100644 keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/open.png create mode 100644 keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/openMostRecent.png create mode 100644 keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/pruneProof.png create mode 100644 keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/saveFile.png diff --git a/keyext.rusty.gui/build.gradle b/keyext.rusty.gui/build.gradle new file mode 100644 index 00000000000..72235a23563 --- /dev/null +++ b/keyext.rusty.gui/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'application' + id 'com.gradleup.shadow' version "9.4.1" +} + +description = "A minimal Swing GUI for the Rusty prover (MVP)" + +repositories { + mavenCentral() +} + +dependencies { + api project(':keyext.rusty') + + implementation 'org.jspecify:jspecify:1.0.0' + implementation 'ch.qos.logback:logback-classic:1.5.32' + implementation 'com.formdev:flatlaf:3.7.1' + + testImplementation(platform('org.junit:junit-bom:5.10.0')) + testImplementation('org.junit.jupiter:junit-jupiter') +} + +application { + mainClass = "org.key_project.rusty.gui.RustyMain" +} + +test { + useJUnitPlatform() +} + +shadowJar { + archiveClassifier = "exe" + archiveBaseName = "keyext.rusty.gui" +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/EditorArea.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/EditorArea.java new file mode 100644 index 00000000000..8317b2704ce --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/EditorArea.java @@ -0,0 +1,283 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Cursor; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; + +import org.key_project.rusty.proof.Goal; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; + +import org.jspecify.annotations.Nullable; + +/// The central editor: a tabbed area of sequent views. One non-closable main tab follows the +/// selected node (its title says whether that node is an open goal or an inner node); further nodes +/// open as closable, pinned tabs (from the proof tree's right-click menu). A node can open *to the +/// side*, which splits the editor into two tab groups, and any pinned view can be moved between the +/// group and the split (or closed) from its tab menu. +public final class EditorArea extends JPanel implements ProofContext.Listener { + + private final ProofContext context; + private final Runnable runProverAction; + private final List views = new ArrayList<>(); + private final SequentView mainView; + private final JLabel mainTabLabel = new JLabel("Sequent"); + + private final JTabbedPane leftTabs = new JTabbedPane(); + private @Nullable JTabbedPane rightTabs; + private @Nullable JSplitPane split; + private JTabbedPane activeTabs; + private Font contentFont = new Font(Font.MONOSPACED, Font.PLAIN, 13); + + public EditorArea(ProofContext context, Runnable runProverAction) { + super(new BorderLayout()); + this.context = context; + this.runProverAction = runProverAction; + // A hairline on the editor's left edge so its tab strip does not meld into the proof-tree + // column across the thin split divider. + setBorder(BorderFactory.createMatteBorder(0, 1, 0, 0, Theme.hairline())); + + // Mark the live (selection-following) tab with the accent colour + bold + a dot, so it is + // obvious which pane tracks the proof-tree selection and which are pinned to a fixed node. + mainTabLabel.setForeground(Theme.accent()); + mainTabLabel.setFont(mainTabLabel.getFont().deriveFont(Font.BOLD)); + mainTabLabel.setToolTipText("This pane follows the selected proof node"); + + mainView = newView(); + leftTabs.addTab("", mainView); + leftTabs.setTabComponentAt(0, tabComponent(mainTabLabel, null)); + wireGroup(leftTabs); + activeTabs = leftTabs; + add(leftTabs, BorderLayout.CENTER); + + context.addListener(this); + updateMainTab(); + } + + @Override + public void proofLoaded() { + updateMainTab(); + } + + @Override + public void proofChanged() { + updateMainTab(); + } + + @Override + public void selectedNodeChanged() { + updateMainTab(); + } + + /// Labels the main tab by what it currently shows: an open goal or an inner node. A leading dot + /// marks it as the live, selection-following pane. + private void updateMainTab() { + Node node = context.getSelectedNode(); + mainTabLabel.setText("● " + (node == null ? "Sequent" : nodeLabel(node))); + } + + /// A node's tab/title label, using the same vocabulary everywhere (goal vs. inner node). + private String nodeLabel(Node node) { + return (isGoal(node) ? "Open goal " : "Inner node ") + node.getSerialNr(); + } + + private boolean isGoal(Node node) { + Proof proof = context.getProof(); + if (proof != null) { + for (Goal goal : proof.openGoals()) { + if (goal.getNode() == node) { + return true; + } + } + } + return false; + } + + /// Opens `node` in a pinned editor tab — in a new split to the side when `toSide`, otherwise as + /// a tab in the active group. + public void openNode(Node node, boolean toSide) { + JTabbedPane target; + if (toSide) { + ensureSplit(); + target = rightTabs; + } else { + target = activeTabs; + } + SequentView view = newView(); + view.pinTo(node); + addClosableTab(target, view); + target.setSelectedComponent(view); + activeTabs = target; + } + + /// Sets the font (family + size) of every editor view. + public void applyFont(Font font) { + this.contentFont = font; + for (SequentView view : views) { + view.applyFont(font); + } + } + + private SequentView newView() { + SequentView view = new SequentView(context); + view.setRunProverAction(runProverAction); + view.applyFont(contentFont); + views.add(view); + return view; + } + + // ── tab groups ────────────────────────────────────────────────────────── + + private void wireGroup(JTabbedPane tabs) { + // Left-align tabs (Aqua centres a lone tab) and scroll, rather than wrap, when there are + // many. + tabs.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT); + // A slimmer tab strip (FlatLaf's default is fairly tall). + tabs.putClientProperty("JTabbedPane.tabHeight", 26); + tabs.addChangeListener(e -> activeTabs = tabs); + tabs.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + onTabMouse(tabs, e); + } + + @Override + public void mouseReleased(MouseEvent e) { + onTabMouse(tabs, e); + } + }); + } + + private void onTabMouse(JTabbedPane tabs, MouseEvent e) { + activeTabs = tabs; + if (!e.isPopupTrigger()) { + return; + } + int idx = tabs.indexAtLocation(e.getX(), e.getY()); + if (idx < 0 || !(tabs.getComponentAt(idx) instanceof SequentView view) + || view == mainView) { + return; + } + JPopupMenu menu = new JPopupMenu(); + JMenuItem move = new JMenuItem(tabs == leftTabs && split == null + ? "Move to a new split" + : "Move to other split"); + move.addActionListener(ev -> moveToOtherSide(tabs, view)); + JMenuItem close = new JMenuItem("Close"); + close.addActionListener(ev -> closeTab(tabs, view)); + menu.add(move); + menu.add(close); + menu.show(tabs, e.getX(), e.getY()); + } + + private void addClosableTab(JTabbedPane tabs, SequentView view) { + String title = titleFor(view); + tabs.addTab(title, view); + int idx = tabs.indexOfComponent(view); + tabs.setTabComponentAt(idx, tabComponent(new JLabel(title), () -> closeTab(tabs, view))); + } + + private String titleFor(SequentView view) { + Node node = view.pinnedNode(); + return node != null ? nodeLabel(node) : view.tabTitle(); + } + + /// A uniform tab component (used for both the main and the pinned tabs so their heights line + /// up): + /// a title label and, when `onClose` is given, a small ✕ close affordance. + private JComponent tabComponent(JLabel title, @Nullable Runnable onClose) { + JPanel tab = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 1)); + tab.setOpaque(false); + tab.add(title); + if (onClose != null) { + JLabel close = new JLabel("✕"); + close.setBorder(BorderFactory.createEmptyBorder(0, 4, 0, 0)); + close.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + close.setToolTipText("Close"); + close.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + onClose.run(); + } + }); + tab.add(close); + } + return tab; + } + + private void closeTab(JTabbedPane tabs, SequentView view) { + if (view == mainView) { + return; + } + tabs.remove(view); + views.remove(view); + if (tabs == rightTabs && rightTabs.getTabCount() == 0) { + collapseSplit(); + } + } + + private void moveToOtherSide(JTabbedPane from, SequentView view) { + JTabbedPane to; + if (from == leftTabs) { + ensureSplit(); + to = rightTabs; + } else { + to = leftTabs; + } + from.remove(view); + addClosableTab(to, view); + to.setSelectedComponent(view); + activeTabs = to; + if (from == rightTabs && rightTabs.getTabCount() == 0) { + collapseSplit(); + } + } + + private void ensureSplit() { + if (split != null) { + return; + } + JTabbedPane right = new JTabbedPane(); + wireGroup(right); + rightTabs = right; + remove(leftTabs); + JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftTabs, right); + sp.setResizeWeight(0.5); + sp.setOneTouchExpandable(true); + split = sp; + add(sp, BorderLayout.CENTER); + revalidate(); + repaint(); + SwingUtilities.invokeLater(() -> sp.setDividerLocation(0.5)); + } + + private void collapseSplit() { + if (split == null) { + return; + } + remove(split); + split = null; + rightTabs = null; + add(leftTabs, BorderLayout.CENTER); + activeTabs = leftTabs; + revalidate(); + repaint(); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/GoalsView.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/GoalsView.java new file mode 100644 index 00000000000..38689650fa3 --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/GoalsView.java @@ -0,0 +1,92 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; + +import org.key_project.rusty.proof.Goal; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; +import org.key_project.rusty.proof.io.OutputStreamProofSaver; + +/// Lists the open goals of the current proof. Selecting a goal selects its node in the context (and +/// thus in the proof tree and sequent view). +public final class GoalsView extends JPanel implements ProofContext.Listener { + + private final ProofContext context; + private final DefaultListModel model = new DefaultListModel<>(); + private final JList list = new JList<>(model); + private boolean syncing; + + public GoalsView(ProofContext context) { + super(new BorderLayout()); + this.context = context; + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setCellRenderer((jl, goal, idx, sel, focus) -> { + var label = new javax.swing.JLabel(describe(goal)); + label.setToolTipText(label.getText()); + label.setBorder(javax.swing.BorderFactory.createEmptyBorder(1, 4, 1, 4)); + label.setOpaque(true); + if (sel) { + label.setBackground(jl.getSelectionBackground()); + label.setForeground(jl.getSelectionForeground()); + } + return label; + }); + list.addListSelectionListener(e -> { + if (syncing || e.getValueIsAdjusting()) { + return; + } + Goal g = list.getSelectedValue(); + if (g != null) { + context.setSelectedNode(g.getNode()); + } + }); + add(new JScrollPane(list), BorderLayout.CENTER); + context.addListener(this); + } + + /// Sets the goals list font (family + size). + public void applyFont(java.awt.Font font) { + list.setFont(font); + } + + @Override + public void proofLoaded() { + refresh(); + } + + @Override + public void proofChanged() { + refresh(); + } + + private void refresh() { + model.clear(); + Proof proof = context.getProof(); + if (proof != null) { + for (Goal g : proof.openGoals()) { + model.addElement(g); + } + } + } + + /// A one-line label for a goal: the node number plus an abbreviated, whitespace-collapsed + /// rendering of its sequent, so goals can be told apart at a glance. + private static String describe(Goal goal) { + Node node = goal.getNode(); + String sequent = + OutputStreamProofSaver.printSequent(node.sequent(), node.proof().getServices()) + .replaceAll("\\s+", " ").trim(); + if (sequent.length() > 90) { + sequent = sequent.substring(0, 89) + "…"; + } + return node.getSerialNr() + ": " + sequent; + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/MainWindow.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/MainWindow.java new file mode 100644 index 00000000000..b7c8ef84b4f --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/MainWindow.java @@ -0,0 +1,465 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.Image; +import java.io.File; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.Preferences; +import javax.swing.BorderFactory; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JSplitPane; +import javax.swing.JToolBar; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.border.Border; +import javax.swing.filechooser.FileNameExtensionFilter; + +import org.key_project.rusty.control.KeYEnvironment; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; +import org.key_project.rusty.proof.io.ProofSaver; + +import org.jspecify.annotations.Nullable; + +/// The single-window MVP over one [ProofContext]: a menu/toolbar (load/save/run), a status bar with +/// a zoom slider, and an IDE-like docked layout. The editor (sequent tabs) is the centre; the proof +/// tree and the open goals — the two navigation lists — are stacked in the left column; the live +/// strategy settings are the right tool window; the node info is the full-width bottom strip. The +/// tool windows are resizable/collapsible via the one-touch split dividers. No multi-proof task +/// management, no KeY-Java specific tooling. +public final class MainWindow extends JFrame { + + private static final int MAX_RECENT = 12; + private static final String RECENT_KEY = "recentFiles"; + private static final int BASE_FONT_SIZE = 13; // font size at 100% zoom + + private final ProofContext context = new ProofContext(); + private final Preferences prefs = Preferences.userNodeForPackage(MainWindow.class); + private final JMenu recentMenu = new JMenu("Recent files"); + private final JLabel statusLabel = new JLabel("No proof loaded"); + private final StrategyView strategyView = new StrategyView(context); + private final ProofTreePanel treePanel = new ProofTreePanel(context); + private final GoalsView goalsView = new GoalsView(context); + private final NodeInfoView infoView = new NodeInfoView(context); + private final EditorArea editor = new EditorArea(context, this::runAutoMode); + private int zoomPercent = 100; + + public MainWindow() { + super("KeYther"); + setDefaultCloseOperation(EXIT_ON_CLOSE); + setJMenuBar(buildMenuBar()); + + treePanel.setHoverListener(infoView::preview); + treePanel.setOpenNodeListener(editor::openNode); + treePanel.setPruneListener(this::prune); + + Icon logo = icon("key-color-icon-square.png", 0); // unscaled, for the window icon + if (logo instanceof ImageIcon img) { + setIconImage(img.getImage()); + } + + // IDE-like docked layout. The editor (sequent tabs) is the centre, with its own header. + // The left column stacks the two *navigation lists* — the proof tree and the open goals — + // because both want vertical room (a proof can have many goals). The node info is short but + // wide content (a taclet line is wide), so it gets the full-width bottom strip. The + // strategy + // settings are an always-visible right tool window. All dividers are one-touch collapsible. + JSplitPane leftColumn = new JSplitPane(JSplitPane.VERTICAL_SPLIT, + toolWindow("Proof tree", treePanel), toolWindow("Open goals", goalsView)); + leftColumn.setResizeWeight(0.6); + leftColumn.setDividerLocation(260); // give the goals list real height by default + leftColumn.setOneTouchExpandable(true); + + // Editor (grows) on the left, strategy tool window pinned to the right. + JSplitPane centreRight = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, editor, + toolWindow("Strategy", strategyView)); + centreRight.setResizeWeight(1.0); + centreRight.setOneTouchExpandable(true); + + JSplitPane editorSplit = + new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftColumn, centreRight); + editorSplit.setDividerLocation(300); + editorSplit.setOneTouchExpandable(true); + + JSplitPane mainSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, editorSplit, + toolWindow("Node info", infoView)); + mainSplit.setResizeWeight(1.0); + mainSplit.setOneTouchExpandable(true); + + add(buildToolBar(), BorderLayout.NORTH); + add(mainSplit, BorderLayout.CENTER); + add(buildStatusBar(), BorderLayout.SOUTH); + applyAllFonts(); + + // Sensible initial divider positions once the frame has a size (fractions need a realised + // component). The strategy pane sits ~230px from the right; node info is a short bottom + // band. + SwingUtilities.invokeLater(() -> { + centreRight.setDividerLocation(centreRight.getWidth() - 230); + mainSplit.setDividerLocation(mainSplit.getHeight() - 150); + }); + + // Keep the status bar in sync with the proof state. + context.addListener(new ProofContext.Listener() { + @Override + public void proofLoaded() { + updateStatus(); + } + + @Override + public void proofChanged() { + updateStatus(); + } + }); + + rebuildRecentMenu(); + setSize(1100, 700); + setLocationRelativeTo(null); + } + + /// Reflects the live proof state (node count, remaining goals, closed) in the status bar. + private void updateStatus() { + Proof proof = context.getProof(); + if (proof == null) { + statusLabel.setText("No proof loaded"); + return; + } + int nodes = proof.countNodes(); + int open = proof.openGoals().size(); + String state = proof.closed() ? "proof closed ✓" + : open + (open == 1 ? " open goal" : " open goals"); + statusLabel.setText(nodes + (nodes == 1 ? " node" : " nodes") + " · " + state); + } + + private JMenuBar buildMenuBar() { + JMenuBar bar = new JMenuBar(); + + JMenu file = new JMenu("File"); + file.add(menuItem("Open .key / .proof ...", this::openProof)); + file.add(menuItem("Reopen most recent", this::openMostRecent)); + file.add(recentMenu); + file.add(menuItem("Save proof ...", this::saveProof)); + file.addSeparator(); + file.add(menuItem("Preferences ...", this::openPreferences)); + file.add(menuItem("Exit", this::dispose)); + + JMenu proof = new JMenu("Proof"); + proof.add(menuItem("Run auto mode", this::runAutoMode)); + proof.add(menuItem("Prune proof at selected node", this::pruneAtSelected)); + + bar.add(file); + bar.add(proof); + return bar; + } + + private JToolBar buildToolBar() { + JToolBar bar = new JToolBar(); + bar.setFloatable(false); + // A thin band between the menu bar and the content, so the toolbar reads as its own strip. + bar.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 1, 0, Theme.hairline()), + BorderFactory.createEmptyBorder(3, 6, 3, 6))); + bar.add(toolButton("Open", "Load a .key problem or .proof", icon("open.png", 18), + this::openProof)); + bar.add(toolButton("Reopen", "Reopen the most recently opened file", + icon("openMostRecent.png", 18), this::openMostRecent)); + bar.add(toolButton("Save", "Save the current proof", icon("saveFile.png", 18), + this::saveProof)); + bar.addSeparator(); + bar.add(toolButton("Run prover", "Run auto mode on the current proof", + icon("autoModeStart.png", 18), this::runAutoMode)); + bar.add(toolButton("Prune", "Prune the proof at the selected node", + icon("pruneProof.png", 18), this::pruneAtSelected)); + return bar; + } + + /// Loads a bundled KeY icon by name, scaled to `size` px (or unscaled when `size <= 0`); + /// returns + /// `null` when the resource is missing. + private @Nullable Icon icon(String name, int size) { + URL url = MainWindow.class.getResource("/org/key_project/rusty/gui/icons/" + name); + if (url == null) { + return null; + } + ImageIcon raw = new ImageIcon(url); + if (size <= 0) { + return raw; + } + return new ImageIcon(raw.getImage().getScaledInstance(size, size, Image.SCALE_SMOOTH)); + } + + /// Wraps a section in an IDE-like tool window: a thin title bar above the content. + private JComponent toolWindow(String title, JComponent content) { + JPanel panel = new JPanel(new BorderLayout()); + + JLabel titleBar = new JLabel(title.toUpperCase()); + titleBar.setHorizontalAlignment(SwingConstants.LEFT); + titleBar.setFont(titleBar.getFont().deriveFont(Font.BOLD, 11f)); + titleBar.setForeground(Theme.mutedText()); + titleBar.setOpaque(true); + titleBar.setBackground(Theme.surface()); + Border line = BorderFactory.createMatteBorder(0, 0, 1, 0, Theme.hairline()); + Border pad = BorderFactory.createEmptyBorder(3, 8, 3, 8); + titleBar.setBorder(BorderFactory.createCompoundBorder(line, pad)); + + panel.add(titleBar, BorderLayout.NORTH); + panel.add(content, BorderLayout.CENTER); + return panel; + } + + private JComponent buildStatusBar() { + JPanel bar = new JPanel(new BorderLayout()); + // A hairline above the status bar separates it from the panes. + bar.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(1, 0, 0, 0, Theme.hairline()), + BorderFactory.createEmptyBorder(3, 8, 3, 8))); + + JPanel zoom = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); + JSlider slider = new JSlider(50, 200, zoomPercent); + slider.setPreferredSize(new Dimension(150, slider.getPreferredSize().height)); + slider.setToolTipText("Zoom (percentage of the configured font size)"); + JLabel value = new JLabel(zoomPercent + "%"); + slider.addChangeListener(e -> { + zoomPercent = slider.getValue(); + value.setText(zoomPercent + "%"); + applyAllFonts(); + }); + zoom.add(new JLabel("Zoom")); + zoom.add(slider); + zoom.add(value); + + bar.add(statusLabel, BorderLayout.WEST); + bar.add(zoom, BorderLayout.EAST); + return bar; + } + + /// Applies the configured fonts (global, with optional per-pane overrides, scaled by the zoom + /// percentage) to every pane. + private void applyAllFonts() { + treePanel.applyFont(fontFor("tree")); + editor.applyFont(fontFor("sequent")); + infoView.applyFont(fontFor("info")); + goalsView.applyFont(fontFor("goals")); + } + + /// The effective font for a pane: its per-pane override family/size if set, otherwise the + /// global + /// family/size, scaled by the current zoom percentage. + private Font fontFor(String pane) { + String globalFamily = prefs.get("font.family", Font.MONOSPACED); + int globalSize = prefs.getInt("font.size", BASE_FONT_SIZE); + String family = prefs.get("font." + pane + ".family", globalFamily); + int base = prefs.getInt("font." + pane + ".size", globalSize); + int size = Math.max(6, Math.round(base * zoomPercent / 100f)); + return new Font(family, Font.PLAIN, size); + } + + private void openPreferences() { + new PreferencesDialog(this, prefs, this::applyAllFonts).setVisible(true); + } + + private JMenuItem menuItem(String label, Runnable action) { + JMenuItem item = new JMenuItem(label); + item.addActionListener(e -> action.run()); + return item; + } + + private JButton toolButton(String label, String tip, @Nullable Icon icon, Runnable action) { + JButton button = new JButton(label); + if (icon != null) { + button.setIcon(icon); + } + button.setToolTipText(tip); + button.setFocusable(false); + button.addActionListener(e -> action.run()); + return button; + } + + // ── Recent files ──────────────────────────────────────────────────────── + + private List recentFiles() { + String stored = prefs.get(RECENT_KEY, ""); + return stored.isEmpty() ? new ArrayList<>() + : new ArrayList<>(Arrays.asList(stored.split("\n"))); + } + + private void addRecentFile(File file) { + String path = file.getAbsolutePath(); + List recent = recentFiles(); + recent.remove(path); + recent.add(0, path); + while (recent.size() > MAX_RECENT) { + recent.remove(recent.size() - 1); + } + prefs.put(RECENT_KEY, String.join("\n", recent)); + rebuildRecentMenu(); + } + + private void rebuildRecentMenu() { + recentMenu.removeAll(); + List recent = recentFiles(); + if (recent.isEmpty()) { + JMenuItem none = new JMenuItem("(none)"); + none.setEnabled(false); + recentMenu.add(none); + return; + } + for (String path : recent) { + File f = new File(path); + JMenuItem item = new JMenuItem(f.getName()); + item.setToolTipText(path); + item.addActionListener(e -> openProof(f)); + recentMenu.add(item); + } + recentMenu.addSeparator(); + recentMenu.add(menuItem("Clear recent files", () -> { + prefs.remove(RECENT_KEY); + rebuildRecentMenu(); + })); + } + + private @Nullable File lastDirectory() { + List recent = recentFiles(); + return recent.isEmpty() ? null : new File(recent.get(0)).getParentFile(); + } + + // ── Actions ───────────────────────────────────────────────────────────── + + private void openProof() { + JFileChooser chooser = new JFileChooser(lastDirectory()); + chooser.setFileFilter( + new FileNameExtensionFilter("KeY problem or proof (*.key, *.proof)", "key", "proof")); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + openProof(chooser.getSelectedFile()); + } + } + + private void openProof(File file) { + try { + KeYEnvironment env = KeYEnvironment.load(file); + Proof proof = env.getLoadedProof(); + if (proof == null) { + showError("No proof was loaded from " + file); + return; + } + context.setProof(env, proof); + addRecentFile(file); + } catch (Exception ex) { + showError("Could not load " + file + ":\n" + ex.getMessage()); + } + } + + private void saveProof() { + Proof proof = context.getProof(); + if (proof == null) { + showError("No proof is open."); + return; + } + JFileChooser chooser = new JFileChooser(lastDirectory()); + chooser.setFileFilter(new FileNameExtensionFilter("KeY proof (*.proof)", "proof")); + if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) { + return; + } + File file = chooser.getSelectedFile(); + if (!file.getName().endsWith(".proof")) { + file = new File(file.getParentFile(), file.getName() + ".proof"); + } + try { + ProofSaver.saveToFile(file, proof); + addRecentFile(file); + } catch (Exception ex) { + showError("Could not save proof:\n" + ex.getMessage()); + } + } + + /// Re-opens the file at the top of the recent-files list. + private void openMostRecent() { + List recent = recentFiles(); + if (recent.isEmpty()) { + showError("No recently opened files."); + return; + } + openProof(new File(recent.get(0))); + } + + /// Prunes the proof at the currently selected node (the node becomes an open goal again). + private void pruneAtSelected() { + Node node = context.getSelectedNode(); + if (node == null) { + showError("No node is selected."); + return; + } + if (node.childrenCount() == 0) { + showError("Nothing to prune: the selected node is a leaf."); + return; + } + prune(node); + } + + /// Prunes the proof at `node`, dropping its subtree, then refreshes the views. + private void prune(Node node) { + Proof proof = context.getProof(); + if (proof == null) { + return; + } + try { + proof.pruneProof(node); + context.fireProofChanged(); + context.setSelectedNode(node); + } catch (Exception ex) { + showError("Could not prune the proof:\n" + ex.getMessage()); + } + } + + private void runAutoMode() { + Proof proof = context.getProof(); + KeYEnvironment env = context.getEnvironment(); + if (proof == null || env == null) { + showError("No proof is open."); + return; + } + // Lock the strategy controls so a live edit cannot mutate the settings mid-run, then run + // the (blocking) strategy off the EDT and refresh the views. + strategyView.setRunning(true); + new Thread(() -> { + try { + env.getProofControl().startAndWaitForAutoMode(proof); + } catch (Exception ex) { + SwingUtilities.invokeLater(() -> { + strategyView.setRunning(false); + showError("Auto mode failed:\n" + ex.getMessage()); + }); + return; + } + SwingUtilities.invokeLater(() -> { + strategyView.setRunning(false); + context.fireProofChanged(); + }); + }, "solidity-auto-mode").start(); + } + + private void showError(String message) { + JOptionPane.showMessageDialog(this, message, "Error", JOptionPane.ERROR_MESSAGE); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/NodeInfoView.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/NodeInfoView.java new file mode 100644 index 00000000000..916f36a57b0 --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/NodeInfoView.java @@ -0,0 +1,168 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Font; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingConstants; + +import org.key_project.prover.rules.RuleApp; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.rule.TacletApp; + +import org.jspecify.annotations.Nullable; + +/// A read-only view describing the rule applied at a proof node. It *pins* to the selected node and +/// *previews* whichever node the cursor hovers in the proof tree, reverting to the pinned node when +/// the cursor leaves. When the rule is a taclet, the full taclet definition is available under a +/// collapsible toggle that shows only the taclet name when collapsed and the whole taclet when +/// expanded. +public final class NodeInfoView extends JPanel implements ProofContext.Listener { + + private final ProofContext context; + private final JTextArea info = new JTextArea(); + private final JButton toggle = new JButton(); + private final JTextArea tacletText = new JTextArea(); + private final JScrollPane tacletScroll; + + private boolean expanded; + private @Nullable String tacletName; + private @Nullable Node shown; + private Font contentFont = new Font(Font.MONOSPACED, Font.PLAIN, 12); + private final Color normalForeground; + + public NodeInfoView(ProofContext context) { + super(new BorderLayout()); + this.context = context; + + info.setEditable(false); + info.setFocusable(false); + info.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + info.setMargin(new java.awt.Insets(6, 8, 2, 8)); + normalForeground = info.getForeground(); + + toggle.setHorizontalAlignment(SwingConstants.LEFT); + toggle.setBorderPainted(false); + toggle.setContentAreaFilled(false); + toggle.setFocusable(false); + toggle.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + toggle.setVisible(false); + toggle.addActionListener(e -> { + expanded = !expanded; + updateTacletSection(); + }); + + tacletText.setEditable(false); + tacletText.setFocusable(false); + tacletText.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + tacletText.setMargin(new java.awt.Insets(2, 14, 4, 6)); + tacletScroll = new JScrollPane(tacletText); + tacletScroll.setBorder(BorderFactory.createEmptyBorder()); + tacletScroll.setVisible(false); + + JPanel top = new JPanel(new BorderLayout()); + top.add(info, BorderLayout.CENTER); + top.add(toggle, BorderLayout.SOUTH); + add(top, BorderLayout.NORTH); + add(tacletScroll, BorderLayout.CENTER); + context.addListener(this); + } + + /// Pins the view to the selected node. + @Override + public void selectedNodeChanged() { + render(context.getSelectedNode()); + } + + @Override + public void proofLoaded() { + render(context.getSelectedNode()); + } + + @Override + public void proofChanged() { + render(context.getSelectedNode()); + } + + /// Previews `node` (e.g. while hovering the proof tree); reverts to the pinned, selected node + /// once the cursor leaves (`node == null`). + public void preview(@Nullable Node node) { + render(node != null ? node : context.getSelectedNode()); + } + + private void render(@Nullable Node node) { + shown = node; + if (node == null) { + info.setForeground(Theme.mutedText()); + info.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, contentFont.getSize())); + info.setText( + "No node selected.\nHover or select a proof node to see its applied rule."); + } else { + info.setForeground(normalForeground); + info.setFont(contentFont); + info.setText(describe(node)); + } + info.setCaretPosition(0); + + RuleApp app = node == null ? null : node.getAppliedRuleApp(); + if (app instanceof TacletApp tacletApp) { + tacletName = tacletApp.taclet().name().toString(); + tacletText.setText(tacletApp.taclet().toString()); + tacletText.setCaretPosition(0); + } else { + tacletName = null; + tacletText.setText(""); + } + updateTacletSection(); + } + + /// Sets the font (family + size) of both the info and the taclet text. + public void applyFont(Font font) { + contentFont = font; + tacletText.setFont(font); + render(shown); + } + + private void updateTacletSection() { + boolean hasTaclet = tacletName != null; + toggle.setVisible(hasTaclet); + if (hasTaclet) { + toggle.setText((expanded ? "▼ " : "▶ ") + "Applied taclet: " + tacletName); + } + tacletScroll.setVisible(hasTaclet && expanded); + revalidate(); + repaint(); + } + + private static String describe(@Nullable Node node) { + if (node == null) { + return ""; + } + RuleApp app = node.getAppliedRuleApp(); + if (app == null) { + return "Node " + node.getSerialNr() + "\n(no rule applied — open or closed leaf)"; + } + StringBuilder sb = new StringBuilder(); + sb.append("Node ").append(node.getSerialNr()).append('\n'); + sb.append("Rule: ").append(app.rule().displayName()); + String internal = app.rule().name().toString(); + if (!internal.equals(app.rule().displayName())) { + sb.append(" (").append(internal).append(')'); + } + sb.append('\n'); + sb.append("Kind: ").append(app instanceof TacletApp ? "taclet" : "built-in rule"); + int children = node.childrenCount(); + if (children > 1) { + sb.append('\n').append("Splits into ").append(children).append(" branches"); + } + return sb.toString(); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/PreferencesDialog.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/PreferencesDialog.java new file mode 100644 index 00000000000..692d0e6b8de --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/PreferencesDialog.java @@ -0,0 +1,192 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Frame; +import java.awt.GraphicsEnvironment; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.prefs.Preferences; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; + +/// A modal dialog to configure the fonts of the GUI: a global font family + size applied to every +/// pane, and an optional per-pane override (proof tree, sequent, node info, open goals). Settings +/// are stored in [Preferences] under `font.*`; `onApply` re-applies them to the live panes. +final class PreferencesDialog extends JDialog { + + /// The panes whose font can be overridden: a preferences key and a display label. + private static final String[][] PANES = { + { "tree", "Proof tree" }, + { "sequent", "Sequent" }, + { "info", "Node info" }, + { "goals", "Open goals" }, + }; + + private final Preferences prefs; + private final Runnable onApply; + + private final JComboBox globalFamily; + private final JSpinner globalSize; + private final List override = new ArrayList<>(); + private final List> paneFamily = new ArrayList<>(); + private final List paneSize = new ArrayList<>(); + + PreferencesDialog(Frame owner, Preferences prefs, Runnable onApply) { + super(owner, "Preferences", true); + this.prefs = prefs; + this.onApply = onApply; + + String[] families = fontFamilies(); + String globalFamilyValue = prefs.get("font.family", Font.MONOSPACED); + int globalSizeValue = prefs.getInt("font.size", 13); + + JPanel form = new JPanel(new GridBagLayout()); + form.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + GridBagConstraints g = new GridBagConstraints(); + g.insets = new Insets(3, 4, 3, 4); + g.anchor = GridBagConstraints.WEST; + int row = 0; + + row = header(form, g, row, "Global font (applies to every pane)"); + globalFamily = new JComboBox<>(families); + globalFamily.setSelectedItem(globalFamilyValue); + globalSize = new JSpinner(new SpinnerNumberModel(globalSizeValue, 6, 48, 1)); + row = fontRow(form, g, row, new JLabel("Font"), globalFamily, globalSize); + + row = separator(form, g, row); + row = header(form, g, row, "Per-pane overrides"); + + for (String[] pane : PANES) { + String key = pane[0]; + boolean has = prefs.get("font." + key + ".family", null) != null; + JCheckBox check = new JCheckBox(pane[1]); + check.setSelected(has); + JComboBox family = new JComboBox<>(families); + family.setSelectedItem(prefs.get("font." + key + ".family", globalFamilyValue)); + JSpinner size = new JSpinner(new SpinnerNumberModel( + prefs.getInt("font." + key + ".size", globalSizeValue), 6, 48, 1)); + override.add(check); + paneFamily.add(family); + paneSize.add(size); + int idx = override.size() - 1; + check.addActionListener(e -> updateEnabled(idx)); + row = fontRow(form, g, row, check, family, size); + updateEnabled(idx); + } + + JButton ok = new JButton("OK"); + ok.addActionListener(e -> { + apply(); + dispose(); + }); + JButton cancel = new JButton("Cancel"); + cancel.addActionListener(e -> dispose()); + JPanel buttons = new JPanel(); + buttons.setLayout(new BoxLayout(buttons, BoxLayout.LINE_AXIS)); + buttons.setBorder(BorderFactory.createEmptyBorder(0, 12, 12, 12)); + buttons.add(Box.createHorizontalGlue()); + buttons.add(cancel); + buttons.add(Box.createHorizontalStrut(6)); + buttons.add(ok); + + add(new JScrollPane(form), BorderLayout.CENTER); + add(buttons, BorderLayout.SOUTH); + getRootPane().setDefaultButton(ok); + pack(); + setLocationRelativeTo(owner); + } + + private int header(JPanel form, GridBagConstraints g, int row, String text) { + JLabel label = new JLabel(text); + label.setFont(label.getFont().deriveFont(Font.BOLD)); + g.gridx = 0; + g.gridy = row; + g.gridwidth = 3; + form.add(label, g); + g.gridwidth = 1; + return row + 1; + } + + private int separator(JPanel form, GridBagConstraints g, int row) { + g.gridx = 0; + g.gridy = row; + g.gridwidth = 3; + g.fill = GridBagConstraints.HORIZONTAL; + form.add(new JSeparator(), g); + g.fill = GridBagConstraints.NONE; + g.gridwidth = 1; + return row + 1; + } + + private int fontRow(JPanel form, GridBagConstraints g, int row, JLabel label, + JComboBox family, JSpinner size) { + g.gridy = row; + g.gridx = 0; + form.add(label, g); + g.gridx = 1; + form.add(family, g); + g.gridx = 2; + form.add(size, g); + return row + 1; + } + + private int fontRow(JPanel form, GridBagConstraints g, int row, JCheckBox check, + JComboBox family, JSpinner size) { + g.gridy = row; + g.gridx = 0; + form.add(check, g); + g.gridx = 1; + form.add(family, g); + g.gridx = 2; + form.add(size, g); + return row + 1; + } + + private void updateEnabled(int idx) { + boolean on = override.get(idx).isSelected(); + paneFamily.get(idx).setEnabled(on); + paneSize.get(idx).setEnabled(on); + } + + private void apply() { + prefs.put("font.family", (String) globalFamily.getSelectedItem()); + prefs.putInt("font.size", (Integer) globalSize.getValue()); + for (int i = 0; i < PANES.length; i++) { + String key = PANES[i][0]; + if (override.get(i).isSelected()) { + prefs.put("font." + key + ".family", (String) paneFamily.get(i).getSelectedItem()); + prefs.putInt("font." + key + ".size", (Integer) paneSize.get(i).getValue()); + } else { + prefs.remove("font." + key + ".family"); + prefs.remove("font." + key + ".size"); + } + } + onApply.run(); + } + + private static String[] fontFamilies() { + List all = new ArrayList<>(List.of(Font.MONOSPACED, Font.SANS_SERIF, Font.SERIF)); + all.addAll(Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment() + .getAvailableFontFamilyNames())); + return all.toArray(new String[0]); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofContext.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofContext.java new file mode 100644 index 00000000000..7d2710d1509 --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofContext.java @@ -0,0 +1,76 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.util.ArrayList; +import java.util.List; + +import org.key_project.rusty.control.KeYEnvironment; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; + +import org.jspecify.annotations.Nullable; + +/// The single-proof "mediator" of the minimal GUI. +/// +/// Unlike KeY-Java's mediator this holds at most one open proof and only the few pieces of state +/// the MVP views need: the loaded environment/proof and the currently selected proof node. Views +/// register as [Listener]s and are notified when the proof or the selection changes. +public final class ProofContext { + + /// Notified about changes to the open proof or the selected node. + public interface Listener { + /// A new proof was loaded (or the previous one was closed). + default void proofLoaded() {} + + /// The proof tree changed (a rule was applied, auto mode ran, ...). + default void proofChanged() {} + + /// The selected proof node changed. + default void selectedNodeChanged() {} + } + + private final List listeners = new ArrayList<>(); + + private @Nullable KeYEnvironment environment; + private @Nullable Proof proof; + private @Nullable Node selectedNode; + + public void addListener(Listener l) { + listeners.add(l); + } + + public @Nullable Proof getProof() { + return proof; + } + + public @Nullable KeYEnvironment getEnvironment() { + return environment; + } + + public @Nullable Node getSelectedNode() { + return selectedNode; + } + + /// Replaces the open proof, selects its root and notifies all listeners. + public void setProof(KeYEnvironment environment, Proof proof) { + this.environment = environment; + this.proof = proof; + this.selectedNode = proof.root(); + listeners.forEach(Listener::proofLoaded); + listeners.forEach(Listener::selectedNodeChanged); + } + + public void setSelectedNode(@Nullable Node node) { + if (node != selectedNode) { + this.selectedNode = node; + listeners.forEach(Listener::selectedNodeChanged); + } + } + + /// Signals that the proof tree was modified (e.g. after a rule application or auto mode). + public void fireProofChanged() { + listeners.forEach(Listener::proofChanged); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofTreePanel.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofTreePanel.java new file mode 100644 index 00000000000..5d3030cbf2f --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/ProofTreePanel.java @@ -0,0 +1,451 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Toolkit; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseMotionAdapter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import org.key_project.rusty.proof.Goal; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; + +/// A linearized proof-tree view (see [#buildBranch]) with the KeY-Java hide filters: hide +/// intermediate steps, show only interactive steps, hide closed subtrees and hide subtrees with no +/// automatic goals. Selecting a tree node updates the [ProofContext]; the view re-syncs when the +/// context changes. +public final class ProofTreePanel extends JPanel implements ProofContext.Listener { + + private final ProofContext context; + private final JTree tree = new JTree(new DefaultMutableTreeNode("(no proof)")); + private final Map nodeToTreeNode = new HashMap<>(); + private boolean syncing; + private @org.jspecify.annotations.Nullable Consumer<@org.jspecify.annotations.Nullable Node> hoverListener; + private @org.jspecify.annotations.Nullable BiConsumer openNodeListener; + private @org.jspecify.annotations.Nullable Consumer pruneListener; + + // Hide filters (mirroring de.uka.ilkd.key.gui.prooftree.ProofTreeViewFilter). + private boolean hideIntermediate; + private boolean onlyInteractive; + private boolean hideClosed; + private boolean hideNonAutomaticGoals; + + public ProofTreePanel(ProofContext context) { + super(new BorderLayout()); + this.context = context; + tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + tree.setRootVisible(true); + tree.addTreeSelectionListener(e -> { + if (syncing) { + return; + } + if (tree.getLastSelectedPathComponent() instanceof DefaultMutableTreeNode dmtn) { + Object userObject = dmtn.getUserObject(); + if (userObject instanceof NodeRef ref) { + context.setSelectedNode(ref.node()); + } else if (userObject instanceof BranchRef branch) { + context.setSelectedNode(branch.node()); + } + } + }); + tree.addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + if (hoverListener != null) { + hoverListener.accept(nodeAt(e.getX(), e.getY())); + } + } + }); + tree.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + maybeShowOpenMenu(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + maybeShowOpenMenu(e); + } + }); + add(buildToolbar(), BorderLayout.NORTH); + add(new JScrollPane(tree), BorderLayout.CENTER); + // Let the split divider shrink this pane to whatever the tree needs, not to the width the + // toolbar's controls would otherwise demand. + setMinimumSize(new Dimension(120, 0)); + context.addListener(this); + } + + /// Registers a listener notified about the node under the cursor as the user hovers the tree + /// (used to feed the node-info view); `null` when no node is under the cursor. + public void setHoverListener(Consumer<@org.jspecify.annotations.Nullable Node> listener) { + this.hoverListener = listener; + } + + /// Registers the action invoked from the node's right-click menu to open it in the editor. The + /// boolean is `true` when the node should open to the side (a new editor split) rather than as + /// a + /// tab in the current group. + public void setOpenNodeListener(BiConsumer listener) { + this.openNodeListener = listener; + } + + /// Registers the action invoked from an inner node's right-click menu to prune the proof there. + public void setPruneListener(Consumer listener) { + this.pruneListener = listener; + } + + private void maybeShowOpenMenu(MouseEvent e) { + if (!e.isPopupTrigger() || openNodeListener == null) { + return; + } + Node node = nodeAt(e.getX(), e.getY()); + if (node == null) { + return; + } + context.setSelectedNode(node); + JPopupMenu menu = new JPopupMenu(); + JMenuItem open = new JMenuItem("Open node " + node.getSerialNr() + " in a tab"); + open.addActionListener(ev -> openNodeListener.accept(node, false)); + JMenuItem openSide = new JMenuItem("Open node " + node.getSerialNr() + " to the side"); + openSide.addActionListener(ev -> openNodeListener.accept(node, true)); + menu.add(open); + menu.add(openSide); + // Pruning only makes sense at an inner node (it drops the subtree below it). + if (pruneListener != null && node.childrenCount() > 0) { + menu.addSeparator(); + JMenuItem prune = new JMenuItem("Prune proof at node " + node.getSerialNr()); + prune.addActionListener(ev -> pruneListener.accept(node)); + menu.add(prune); + } + menu.show(tree, e.getX(), e.getY()); + } + + private @org.jspecify.annotations.Nullable Node nodeAt(int x, int y) { + TreePath path = tree.getPathForLocation(x, y); + if (path != null + && path.getLastPathComponent() instanceof DefaultMutableTreeNode dmtn) { + Object userObject = dmtn.getUserObject(); + if (userObject instanceof NodeRef ref) { + return ref.node(); + } + if (userObject instanceof BranchRef branch) { + return branch.node(); + } + } + return null; + } + + /// A compact toolbar grounded by a hairline: an icon row (magnifier + "Filter ▾"), with the + /// search field and the filter checkboxes each in their own full-width row below, shown only + /// when their button is toggled. The filters are plain checkboxes (you can flip several at + /// once) + /// rather than a popup menu that closes after each click. + private JPanel buildToolbar() { + JPanel top = new JPanel(new BorderLayout(0, 4)); + top.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, Theme.hairline()), + BorderFactory.createEmptyBorder(3, 6, 3, 6))); + + JComponent searchRow = buildSearchRow(); + JComponent filterRow = buildFilterRow(); + + JButton searchToggle = toolbarButton("🔍", "Search the proof tree"); + searchToggle.addActionListener(e -> toggleRow(top, searchRow)); + JButton filterToggle = toolbarButton("Filter ▾", "Hide filters"); + filterToggle.addActionListener(e -> toggleRow(top, filterRow)); + + JPanel iconRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); + iconRow.setOpaque(false); + iconRow.add(searchToggle); + iconRow.add(filterToggle); + + JPanel expandable = new JPanel(); + expandable.setOpaque(false); + expandable.setLayout(new BoxLayout(expandable, BoxLayout.PAGE_AXIS)); + expandable.add(searchRow); + expandable.add(filterRow); + + top.add(iconRow, BorderLayout.NORTH); + top.add(expandable, BorderLayout.CENTER); + return top; + } + + private void toggleRow(JComponent toolbar, JComponent row) { + boolean show = !row.isVisible(); + row.setVisible(show); + toolbar.revalidate(); + toolbar.repaint(); + if (show) { + row.requestFocusInWindow(); + } + } + + private JComponent buildSearchRow() { + JTextField field = new JTextField(); + field.setToolTipText("Find a node by label"); + JButton prev = new JButton("▲"); + JButton next = new JButton("▼"); + prev.setToolTipText("Previous match"); + next.setToolTipText("Next match"); + for (JButton b : new JButton[] { prev, next }) { + b.setFocusable(false); + b.setMargin(new java.awt.Insets(1, 6, 1, 6)); + } + field.addActionListener(e -> search(field.getText(), true)); // Enter = next match + next.addActionListener(e -> search(field.getText(), true)); + prev.addActionListener(e -> search(field.getText(), false)); + + JPanel matchButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 2, 0)); + matchButtons.setOpaque(false); + matchButtons.add(prev); + matchButtons.add(next); + + JPanel row = new JPanel(new BorderLayout(4, 0)); + row.setOpaque(false); + row.add(field, BorderLayout.CENTER); // full width of the panel + row.add(matchButtons, BorderLayout.EAST); + row.setAlignmentX(Component.LEFT_ALIGNMENT); + row.setMaximumSize(new Dimension(Integer.MAX_VALUE, row.getPreferredSize().height)); + row.setVisible(false); + return row; + } + + /// A stacked list of filter checkboxes (mirrors + /// de.uka.ilkd.key.gui.prooftree.ProofTreeViewFilter) + /// that fits a narrow pane and lets several be toggled without re-opening. + private JComponent buildFilterRow() { + JPanel row = new JPanel(); + row.setOpaque(false); + row.setLayout(new BoxLayout(row, BoxLayout.PAGE_AXIS)); + row.add(filterCheck("Hide intermediate", v -> hideIntermediate = v)); + row.add(filterCheck("Only interactive", v -> onlyInteractive = v)); + row.add(filterCheck("Hide closed", v -> hideClosed = v)); + row.add(filterCheck("Hide automatic goals", v -> hideNonAutomaticGoals = v)); + row.setAlignmentX(Component.LEFT_ALIGNMENT); + row.setMaximumSize(new Dimension(Integer.MAX_VALUE, row.getPreferredSize().height)); + row.setVisible(false); + return row; + } + + private JCheckBox filterCheck(String label, Consumer setter) { + JCheckBox box = new JCheckBox(label); + box.setOpaque(false); + box.setAlignmentX(Component.LEFT_ALIGNMENT); + box.addActionListener(e -> { + setter.accept(box.isSelected()); + rebuild(); + }); + return box; + } + + private JButton toolbarButton(String label, String tooltip) { + JButton button = new JButton(label); + button.setToolTipText(tooltip); + button.setFocusable(false); + button.setMargin(new java.awt.Insets(2, 6, 2, 6)); + return button; + } + + /// Selects the next/previous tree node (in display order, wrapping around) whose label contains + /// `query` (case-insensitive). Returns whether a match was found. + boolean search(String query, boolean forward) { + String q = query.trim().toLowerCase(); + int rows = tree.getRowCount(); + if (q.isEmpty() || rows == 0) { + return false; + } + int start = Math.max(tree.getLeadSelectionRow(), 0); + for (int k = 1; k <= rows; k++) { + int row = forward ? (start + k) % rows : ((start - k) % rows + rows) % rows; + TreePath path = tree.getPathForRow(row); + if (path != null + && path.getLastPathComponent().toString().toLowerCase().contains(q)) { + tree.setSelectionRow(row); + tree.scrollRowToVisible(row); + return true; + } + } + Toolkit.getDefaultToolkit().beep(); + return false; + } + + /// Package-private access to the underlying tree, for tests. + JTree getTree() { + return tree; + } + + /// Sets the tree's font (family + size). + public void applyFont(java.awt.Font font) { + tree.setFont(font); + tree.setRowHeight(0); // recompute the row height for the new font + } + + /// Package-private filter control, for tests. + void setFilters(boolean intermediate, boolean interactive, boolean closed, boolean autoGoals) { + this.hideIntermediate = intermediate; + this.onlyInteractive = interactive; + this.hideClosed = closed; + this.hideNonAutomaticGoals = autoGoals; + rebuild(); + } + + @Override + public void proofLoaded() { + rebuild(); + } + + @Override + public void proofChanged() { + rebuild(); + } + + @Override + public void selectedNodeChanged() { + Node node = context.getSelectedNode(); + DefaultMutableTreeNode tn = node == null ? null : nodeToTreeNode.get(node); + if (tn == null) { + return; + } + syncing = true; + try { + TreePath path = new TreePath(tn.getPath()); + tree.setSelectionPath(path); + tree.scrollPathToVisible(path); + } finally { + syncing = false; + } + } + + private void rebuild() { + nodeToTreeNode.clear(); + Proof proof = context.getProof(); + DefaultMutableTreeNode root = proof == null ? new DefaultMutableTreeNode("(no proof)") + : buildBranch(proof.root(), "Proof Tree"); + tree.setModel(new DefaultTreeModel(root)); + for (int i = 0; i < tree.getRowCount(); i++) { + tree.expandRow(i); + } + selectedNodeChanged(); + } + + /// Builds one branch of the linearized tree: the maximal linear chain of proof nodes starting + /// at `start` is added as flat leaves, and each child of the chain's final (splitting) node + /// starts a new sub-branch. Nodes hidden by the local filters are dropped from the chain, and + /// sub-branches hidden by the global filters are skipped entirely. + private DefaultMutableTreeNode buildBranch(Node start, String label) { + DefaultMutableTreeNode branch = new DefaultMutableTreeNode(new BranchRef(start, label)); + + List chain = new ArrayList<>(); + Node n = start; + while (true) { + chain.add(n); + if (n.childrenCount() == 1) { + n = n.child(0); + } else { + break; + } + } + Node endpoint = n; + for (Node step : chain) { + if (showChainNode(step, endpoint)) { + DefaultMutableTreeNode leaf = new DefaultMutableTreeNode(new NodeRef(step)); + leaf.setAllowsChildren(false); + nodeToTreeNode.putIfAbsent(step, leaf); + branch.add(leaf); + } else { + // keep the selection resolvable: a hidden step maps to its enclosing branch + nodeToTreeNode.putIfAbsent(step, branch); + } + } + + for (int i = 0; i < endpoint.childrenCount(); i++) { + Node child = endpoint.child(i); + if (hiddenByGlobalFilters(child)) { + continue; + } + String childLabel = child.getNodeInfo().getBranchLabel(); + branch.add(buildBranch(child, childLabel != null ? childLabel : "Case " + (i + 1))); + } + return branch; + } + + /// Whether a node of a linear chain is shown (the chain endpoint is always shown). + private boolean showChainNode(Node step, Node endpoint) { + if (step == endpoint) { + return true; + } + if (onlyInteractive) { + return step.getNodeInfo().getInteractiveRuleApplication(); + } + return !hideIntermediate; + } + + /// Whether the subtree rooted at `node` is hidden by an active global filter. + private boolean hiddenByGlobalFilters(Node node) { + if (hideClosed && node.isClosed()) { + return true; + } + return hideNonAutomaticGoals && !hasAutomaticGoal(node); + } + + private static boolean hasAutomaticGoal(Node node) { + Proof proof = node.proof(); + var goals = proof.getSubtreeGoals(node); + if (goals.isEmpty()) { + return true; // a closed subtree is still shown (matches KeY) + } + for (Goal goal : goals) { + if (goal.isAutomatic()) { + return true; + } + } + return false; + } + + /// Wraps a single proof [Node] (a leaf of the tree); the label is its (single-line) name. + private record NodeRef(Node node) { + @Override + public String toString() { + String name = node.name().replace('\n', ' ').trim(); + return node.getSerialNr() + ": " + name; + } + } + + /// Wraps a proof branch (its first node and a label); shown as an inner tree node. A closed + /// branch is marked. + private record BranchRef(Node node, String label) { + @Override + public String toString() { + String shown = node.getNodeInfo().getBranchLabel(); + String base = shown != null ? shown : label; + return node.isClosed() ? base + " ✓" : base; + } + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/RustyMain.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/RustyMain.java new file mode 100644 index 00000000000..35945d29c65 --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/RustyMain.java @@ -0,0 +1,34 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import javax.swing.SwingUtilities; +import javax.swing.UIManager; + +import com.formdev.flatlaf.FlatLightLaf; + +/// Entry point of KeYther, the minimal Rusty prover GUI. +public final class RustyMain { + + private RustyMain() {} + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + // A modern, flat cross-platform look (away from the native Aqua/Metal styling). + try { + UIManager.put("Component.focusWidth", 1); + UIManager.put("TabbedPane.tabType", "card"); + UIManager.put("ScrollBar.showButtons", false); + FlatLightLaf.setup(); + } catch (Exception ex) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ignored) { + // fall back to the default look and feel + } + } + new MainWindow().setVisible(true); + }); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/SequentView.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/SequentView.java new file mode 100644 index 00000000000..b9db705d020 --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/SequentView.java @@ -0,0 +1,443 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.text.BadLocationException; +import javax.swing.text.DefaultHighlighter; +import javax.swing.text.Highlighter; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.rusty.Services; +import org.key_project.rusty.pp.IdentitySequentPrintFilter; +import org.key_project.rusty.pp.InitialPositionTable; +import org.key_project.rusty.pp.LogicPrinter; +import org.key_project.rusty.pp.NotationInfo; +import org.key_project.rusty.pp.PosInSequent; +import org.key_project.rusty.pp.PosTableLayouter; +import org.key_project.rusty.pp.Range; +import org.key_project.rusty.proof.Goal; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; +import org.key_project.rusty.rule.FindTaclet; +import org.key_project.rusty.rule.NoPosTacletApp; +import org.key_project.rusty.rule.TacletApp; + +import org.jspecify.annotations.Nullable; + +/// Shows the sequent of the selected node (inner or leaf). A small header carries the node/rule +/// info; the sequent itself is rendered with the position-table printer so a left-click maps to the +/// term under the cursor and offers the applicable taclets of an open goal in a popup. Taclets with +/// open schema variables are completed via the [TacletCompletionDialog] before being applied. +public final class SequentView extends JPanel implements ProofContext.Listener { + + private final ProofContext context; + private final JLabel header = new JLabel(" "); + private final JTextArea text = new JTextArea(); + private final IdentitySequentPrintFilter filter = new IdentitySequentPrintFilter(); + + private final Highlighter.HighlightPainter painter = + new DefaultHighlighter.DefaultHighlightPainter(Theme.selection()); + private @Nullable Object highlightTag; + + private @Nullable InitialPositionTable positionTable; + private @Nullable Node node; + private @Nullable Runnable runProverAction; + + /// When pinned to a node this view shows that node; otherwise it follows the selection. + private @Nullable Node pinnedNode; + private boolean follow = true; + private Font contentFont = new Font(Font.MONOSPACED, Font.PLAIN, 13); + private final Color normalForeground; + + public SequentView(ProofContext context) { + super(new BorderLayout()); + this.context = context; + + // Cut from the same cloth as the tool-window title bars (same surface + hairline) so the + // editor reads as part of the family, but carries node/rule data rather than a label. + header.setOpaque(true); + header.setBackground(Theme.surface()); + header.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(0, 0, 1, 0, Theme.hairline()), + BorderFactory.createEmptyBorder(4, 8, 4, 8))); + + text.setEditable(false); + // No text caret: the hover highlight already shows the focused term, and a blinking caret + // next to it is just noise. Making the area non-focusable suppresses the caret while mouse + // highlighting and the popup keep working. + text.setFocusable(false); + text.setFont(contentFont); + text.setMargin(new java.awt.Insets(6, 8, 6, 8)); + normalForeground = text.getForeground(); + MouseAdapter mouse = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e)) { + showTacletPopup(e); + } + } + + @Override + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + showContextMenu(e); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + showContextMenu(e); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + highlightAt(text.viewToModel2D(e.getPoint())); + } + + @Override + public void mouseExited(MouseEvent e) { + clearHighlight(); + } + }; + text.addMouseListener(mouse); + text.addMouseMotionListener(mouse); + + add(header, BorderLayout.NORTH); + add(new JScrollPane(text), BorderLayout.CENTER); + context.addListener(this); + render(); // show the empty-state hint until a proof is loaded + } + + /// Highlights the term/formula whose printed range contains `offset` (the lowest such subterm), + /// or clears the highlight when `offset` does not point inside a term. + private void highlightAt(int offset) { + if (positionTable == null) { + clearHighlight(); + return; + } + PosInSequent pis = positionTable.getPosInSequent(offset, filter); + if (pis == null || pis.isSequent() || pis.getBounds() == null) { + clearHighlight(); + return; + } + var bounds = pis.getBounds(); + try { + clearHighlight(); + highlightTag = + text.getHighlighter().addHighlight(bounds.start(), bounds.end(), painter); + } catch (BadLocationException ignored) { + // range no longer valid (e.g. just re-rendered); ignore + } + } + + private void clearHighlight() { + if (highlightTag != null) { + text.getHighlighter().removeHighlight(highlightTag); + highlightTag = null; + } + } + + @Override + public void selectedNodeChanged() { + render(); + } + + @Override + public void proofLoaded() { + render(); + } + + @Override + public void proofChanged() { + render(); + } + + /// Pins this view to a specific node (it stops following the selection). + public void pinTo(Node node) { + this.pinnedNode = node; + this.follow = false; + render(); + } + + /// A short title for this view's editor tab: the pinned node, or the live selection. + public String tabTitle() { + return pinnedNode != null ? "Node " + pinnedNode.getSerialNr() : "Selection"; + } + + /// The node this view is pinned to, or `null` when it follows the selection. + public @Nullable Node pinnedNode() { + return pinnedNode; + } + + private void render() { + highlightTag = null; // setText below drops all highlights + node = follow ? context.getSelectedNode() : pinnedNode; + if (node == null) { + header.setText(" "); + // A quiet, non-monospace hint — clearly chrome, not a sequent to be read. + text.setForeground(Theme.mutedText()); + text.setFont( + new Font(Font.SANS_SERIF, Font.ITALIC, Math.max(contentFont.getSize(), 13))); + text.setText("\n Open a .key problem or .proof to begin.\n\n" + + " Use File ▸ Open, or the Open button in the toolbar."); + text.setCaretPosition(0); + positionTable = null; + return; + } + // Real content: prominent, in the configured font and normal colour. + text.setForeground(normalForeground); + text.setFont(contentFont); + boolean goal = goalFor(node) != null; + StringBuilder head = new StringBuilder("Node ").append(node.getSerialNr()) + .append(" • ").append(goal ? "open goal" : "inner node"); + if (node.getAppliedRuleApp() != null) { + head.append(" • rule: ").append(node.getAppliedRuleApp().rule().name()); + } + header.setText(head.toString()); + + Services services = node.proof().getServices(); + PosTableLayouter layouter = PosTableLayouter.positionTable(80); + LogicPrinter printer = new LogicPrinter(new NotationInfo(), services, layouter); + printer.printSequent(node.sequent()); + text.setText(printer.result()); + positionTable = layouter.getInitialPositionTable(); + filter.setSequent(node.sequent()); + text.setCaretPosition(0); + } + + /// Left-click popup: the taclets applicable to the formula/term under the cursor. + private void showTacletPopup(MouseEvent e) { + if (positionTable == null || node == null) { + return; + } + Goal goal = goalFor(node); + JPopupMenu menu = new JPopupMenu(); + if (goal == null) { + JMenuItem item = new JMenuItem("(not an open goal)"); + item.setEnabled(false); + menu.add(item); + menu.show(text, e.getX(), e.getY()); + return; + } + + int offset = text.viewToModel2D(e.getPoint()); + highlightAt(offset); // show what the popup targets + PosInSequent pis = positionTable.getPosInSequent(offset, filter); + PosInOccurrence occ = pis == null ? null : pis.getPosInOccurrence(); + + // Rules that apply to the clicked formula/term go first, most-specific match (deepest find + // pattern) on top; the sequent-wide (no-find) rules are tucked away in a submenu so they do + // not bury the term-specific ones. + List termApps = applicableTaclets(goal, occ); + termApps.sort(BY_SPECIFICITY); + List sequentApps = noFindTaclets(goal); + sequentApps.sort(BY_SPECIFICITY); + if (termApps.isEmpty() && sequentApps.isEmpty()) { + JMenuItem item = new JMenuItem("(no applicable rules here)"); + item.setEnabled(false); + menu.add(item); + } else if (termApps.isEmpty()) { + // Whole-sequent selection: no term-specific rules to bury, so show the sequent rules + // directly (chunked into submenus only when there are too many). + addTacletItems(menu, sequentApps, goal, null); + } else { + addTacletItems(menu, termApps, goal, occ); + if (!sequentApps.isEmpty()) { + // A term was clicked: tuck the sequent-wide rules away so they do not bury the + // term-specific ones. + menu.addSeparator(); + JMenu sequentMenu = new JMenu("Sequent rules"); + addTacletItems(sequentMenu, sequentApps, goal, null); + menu.add(sequentMenu); + } + } + menu.show(text, e.getX(), e.getY()); + } + + /// Orders taclet applications by most specific match first (deepest find pattern), breaking + /// ties + /// alphabetically by display name. + private static final Comparator BY_SPECIFICITY = + Comparator.comparingInt(SequentView::specificity).reversed() + .thenComparing(app -> app.rule().displayName()); + + /// A specificity score for ordering: the depth of the taclet's find pattern (no-find taclets, + /// which match the whole sequent, score 0 — the least specific). + private static int specificity(TacletApp app) { + return app.taclet() instanceof FindTaclet find ? find.find().depth() : 0; + } + + /// Adds the apps to `parent`, never showing more than six taclets in one menu: when there are + /// more, the five most specific stay and the rest nest under a "More rules…" submenu + /// (recursively + /// chunked the same way). + private void addTacletItems(JComponent parent, List apps, Goal goal, + @Nullable PosInOccurrence occ) { + if (apps.size() <= 6) { + for (TacletApp app : apps) { + parent.add(tacletItem(app, goal, occ)); + } + return; + } + for (int i = 0; i < 5; i++) { + parent.add(tacletItem(apps.get(i), goal, occ)); + } + JMenu more = new JMenu("More rules…"); + addTacletItems(more, apps.subList(5, apps.size()), goal, occ); + parent.add(more); + } + + private JMenuItem tacletItem(TacletApp app, Goal goal, @Nullable PosInOccurrence occ) { + JMenuItem item = new JMenuItem(app.rule().displayName()); + item.addActionListener(ev -> applyTaclet(app, goal, occ)); + return item; + } + + /// Right-click context menu: general actions (run the prover, copy the term under the cursor). + private void showContextMenu(MouseEvent e) { + if (node == null) { + return; + } + int offset = text.viewToModel2D(e.getPoint()); + highlightAt(offset); + JPopupMenu menu = new JPopupMenu(); + + JMenuItem run = new JMenuItem("Run prover"); + run.setEnabled(runProverAction != null && context.getProof() != null); + run.addActionListener(ev -> { + if (runProverAction != null) { + runProverAction.run(); + } + }); + menu.add(run); + + JMenuItem copy = new JMenuItem("Copy term"); + copy.addActionListener(ev -> copyTerm(offset)); + menu.add(copy); + + menu.show(text, e.getX(), e.getY()); + } + + /// Copies the printed text of the term/formula under `offset` (or the whole sequent if the + /// offset is not inside a term) to the system clipboard. + private void copyTerm(int offset) { + String content = text.getText(); + if (positionTable != null) { + PosInSequent pis = positionTable.getPosInSequent(offset, filter); + if (pis != null && pis.getBounds() != null) { + Range b = pis.getBounds(); + content = content.substring(b.start(), b.end()); + } + } + Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(new StringSelection(content), null); + } + + /// Sets the action invoked by the context menu's "Run prover" entry (wired by the main window). + public void setRunProverAction(@Nullable Runnable runProverAction) { + this.runProverAction = runProverAction; + } + + /// Sets the font (family + size) of the sequent text. + public void applyFont(Font font) { + this.contentFont = font; + render(); // re-applies the right font/colour for content vs. the empty-state hint + } + + /// The open goal sitting on `node`, or `null` if `node` is not an open goal. + private @Nullable Goal goalFor(Node node) { + Proof proof = context.getProof(); + if (proof == null) { + return null; + } + for (Goal g : proof.openGoals()) { + if (g.getNode() == node) { + return g; + } + } + return null; + } + + /// The find-taclet applications at `occ` (the rules that apply to the formula/term under the + /// cursor). Applications with open schema variables are included; they are completed via the + /// taclet-completion dialog when chosen. + List applicableTaclets(Goal goal, @Nullable PosInOccurrence occ) { + List result = new ArrayList<>(); + if (occ != null) { + Services services = goal.getOverlayServices(); + for (TacletApp app : goal.ruleAppIndex().getTacletAppAt(occ, services)) { + result.add(app); + } + } + return result; + } + + /// The no-find (sequent-wide) taclet applications, e.g. cut (which is completed via the + /// dialog). + List noFindTaclets(Goal goal) { + List result = new ArrayList<>(); + for (NoPosTacletApp app : goal.ruleAppIndex().getNoFindTaclet(goal.getOverlayServices())) { + result.add(app); + } + return result; + } + + /// Applies a taclet to the goal, refreshing the views. If the application still has open schema + /// variables, the taclet-completion dialog is shown first; cancelling it aborts the + /// application. + void applyTaclet(TacletApp app, Goal goal, @Nullable PosInOccurrence occ) { + try { + TacletApp toApply = occ != null ? app.setPosInOccurrence(occ, goal.getOverlayServices()) + : app; + if (!toApply.complete()) { + TacletApp completed = TacletCompletionDialog.completeApp(this, toApply, goal); + if (completed == null) { + return; // cancelled or invalid + } + toApply = completed; + } + goal.apply(toApply); + context.fireProofChanged(); + context.setSelectedNode(goal.getNode()); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + "Could not apply " + app.rule().displayName() + ":\n" + + ex.getMessage(), + "Rule application failed", JOptionPane.ERROR_MESSAGE); + } + } + + /// Package-private accessors for tests. + String renderedText() { + return text.getText(); + } + + @Nullable + PosInSequent posAt(int offset) { + return positionTable == null ? null : positionTable.getPosInSequent(offset, filter); + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/StrategyView.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/StrategyView.java new file mode 100644 index 00000000000..8a152752474 --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/StrategyView.java @@ -0,0 +1,250 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.Scrollable; +import javax.swing.SpinnerNumberModel; + +import org.key_project.rusty.proof.Proof; +import org.key_project.rusty.settings.StrategySettings; +import org.key_project.rusty.strategy.StrategyProperties; + +import org.jspecify.annotations.Nullable; + +import static org.key_project.rusty.strategy.StrategyProperties.FUNCTION_CONTRACT; +import static org.key_project.rusty.strategy.StrategyProperties.FUNCTION_EXPAND; +import static org.key_project.rusty.strategy.StrategyProperties.FUNCTION_NONE; +import static org.key_project.rusty.strategy.StrategyProperties.FUNCTION_OPTIONS_KEY; +import static org.key_project.rusty.strategy.StrategyProperties.NON_LIN_ARITH_COMPLETION; +import static org.key_project.rusty.strategy.StrategyProperties.NON_LIN_ARITH_DEF_OPS; +import static org.key_project.rusty.strategy.StrategyProperties.NON_LIN_ARITH_NONE; +import static org.key_project.rusty.strategy.StrategyProperties.NON_LIN_ARITH_OPTIONS_KEY; +import static org.key_project.rusty.strategy.StrategyProperties.SPLITTING_DELAYED; +import static org.key_project.rusty.strategy.StrategyProperties.SPLITTING_NORMAL; +import static org.key_project.rusty.strategy.StrategyProperties.SPLITTING_OFF; +import static org.key_project.rusty.strategy.StrategyProperties.SPLITTING_OPTIONS_KEY; +import static org.key_project.rusty.strategy.StrategyProperties.STOPMODE_DEFAULT; +import static org.key_project.rusty.strategy.StrategyProperties.STOPMODE_NONCLOSE; +import static org.key_project.rusty.strategy.StrategyProperties.STOPMODE_OPTIONS_KEY; + +/// An always-visible tool window for the automatic proof search of the current proof: it shows the +/// live strategy settings (max rule applications, timeout, and the main option groups) and writes +/// every edit straight back to the proof's settings, so there is no open/apply round-trip. It +/// repopulates whenever a proof is loaded and disables itself when none is open. +public final class StrategyView extends JPanel implements ProofContext.Listener { + + /// A single multiple-choice strategy option (a property key and its labelled values). + private record Choice(String label, String key, String[] values, String[] valueLabels) { + } + + private static final Choice[] CHOICES = { + new Choice("Stop at", STOPMODE_OPTIONS_KEY, + new String[] { STOPMODE_DEFAULT, STOPMODE_NONCLOSE }, + new String[] { "Default", "Unclosable goal" }), + new Choice("Splitting", SPLITTING_OPTIONS_KEY, + new String[] { SPLITTING_NORMAL, SPLITTING_DELAYED, SPLITTING_OFF }, + new String[] { "Normal", "Delayed", "Off" }), + new Choice("Functions", FUNCTION_OPTIONS_KEY, + new String[] { FUNCTION_EXPAND, FUNCTION_CONTRACT, FUNCTION_NONE }, + new String[] { "Expand", "Contract", "None" }), + new Choice("Arithmetic", NON_LIN_ARITH_OPTIONS_KEY, + new String[] { NON_LIN_ARITH_NONE, NON_LIN_ARITH_DEF_OPS, NON_LIN_ARITH_COMPLETION }, + new String[] { "Basic", "DefOps", "Model search" }), + }; + + private final ProofContext context; + private final JSpinner maxSteps = + new JSpinner(new SpinnerNumberModel(0, 0, 1_000_000, 100)); + private final JSpinner timeout = + new JSpinner(new SpinnerNumberModel(-1.0, -1.0, Long.MAX_VALUE, 1000)); + private final List> combos = new ArrayList<>(); + private final List controls = new ArrayList<>(); + + /// Guards the change listeners while the controls are repopulated from the settings. + private boolean loading; + + /// While an auto-mode run is in progress the controls are locked, so a live edit can never + /// mutate the settings the prover thread is reading. + private boolean running; + + public StrategyView(ProofContext context) { + super(new BorderLayout()); + this.context = context; + + // Stack the label above its control so each row only needs the width of one control: the + // pane stays usable when narrow instead of forcing a horizontal scrollbar. The form tracks + // the viewport width (see ScrollableForm) so the controls shrink to fit and are never + // clipped on the right. + JPanel form = new ScrollableForm(); + form.setLayout(new BoxLayout(form, BoxLayout.PAGE_AXIS)); + form.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); + + maxSteps.addChangeListener(e -> applyMaxSteps()); + addGroup(form, "Max. rule apps", maxSteps); + controls.add(maxSteps); + + timeout.addChangeListener(e -> applyTimeout()); + timeout.setToolTipText("Timeout in ms; -1 disables it"); + addGroup(form, "Timeout (ms)", timeout); + controls.add(timeout); + + for (Choice c : CHOICES) { + JComboBox combo = new JComboBox<>(c.valueLabels()); + int index = combos.size(); + combo.addActionListener(e -> applyChoice(index)); + combos.add(combo); + controls.add(combo); + addGroup(form, c.label(), combo); + } + + form.add(Box.createVerticalGlue()); // keep the groups pinned to the top + + // No horizontal scrollbar: groups always fit the pane width. + add(new JScrollPane(form, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, + JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), BorderLayout.CENTER); + + // Let the divider drag this pane narrow; it does not demand a wide minimum. + setMinimumSize(new Dimension(150, 0)); + + context.addListener(this); + load(); + } + + @Override + public void proofLoaded() { + load(); + } + + /// Locks the controls while an auto-mode run is in progress and restores them afterwards. + public void setRunning(boolean running) { + this.running = running; + updateEnabled(); + } + + private void addGroup(JPanel form, String label, JComponent field) { + JPanel group = new JPanel(new BorderLayout(0, 2)); + group.setAlignmentX(Component.LEFT_ALIGNMENT); + group.add(new JLabel(label), BorderLayout.NORTH); + group.add(field, BorderLayout.CENTER); + // Let the group grow horizontally but never stretch vertically. + group.setMaximumSize(new Dimension(Integer.MAX_VALUE, group.getPreferredSize().height)); + form.add(group); + form.add(Box.createVerticalStrut(10)); + } + + private @Nullable StrategySettings settings() { + Proof proof = context.getProof(); + return proof == null ? null : proof.getSettings().getStrategySettings(); + } + + /// Repopulates the controls from the current proof's settings (or disables them if none). + private void load() { + StrategySettings settings = settings(); + if (settings != null) { + loading = true; + try { + maxSteps.setValue(Math.max(settings.getMaxSteps(), 0)); + timeout.setValue((double) settings.getTimeout()); + StrategyProperties props = settings.getActiveStrategyProperties(); + for (int i = 0; i < CHOICES.length; i++) { + combos.get(i).setSelectedIndex( + indexOf(CHOICES[i], props.getProperty(CHOICES[i].key()))); + } + } finally { + loading = false; + } + } + updateEnabled(); + } + + private void applyMaxSteps() { + StrategySettings settings = liveSettings(); + if (settings != null) { + settings.setMaxSteps((Integer) maxSteps.getValue()); + } + } + + private void applyTimeout() { + StrategySettings settings = liveSettings(); + if (settings != null) { + settings.setTimeout(((Number) timeout.getValue()).longValue()); + } + } + + private void applyChoice(int i) { + StrategySettings settings = liveSettings(); + if (settings == null) { + return; + } + StrategyProperties props = settings.getActiveStrategyProperties(); + props.setProperty(CHOICES[i].key(), CHOICES[i].values()[combos.get(i).getSelectedIndex()]); + settings.setActiveStrategyProperties(props); + } + + /// The settings to write to from a user edit, or `null` while repopulating or with no proof. + private @Nullable StrategySettings liveSettings() { + return loading ? null : settings(); + } + + /// Controls are usable only when a proof is open and no run is in progress. + private void updateEnabled() { + boolean enabled = !running && settings() != null; + for (JComponent control : controls) { + control.setEnabled(enabled); + } + } + + private static int indexOf(Choice c, String value) { + for (int i = 0; i < c.values().length; i++) { + if (c.values()[i].equals(value)) { + return i; + } + } + return 0; + } + + /// A form panel that is as wide as the scroll viewport (never wider), so its controls shrink to + /// fit the pane and are never clipped on the right; it still scrolls vertically when too tall. + private static final class ScrollableForm extends JPanel implements Scrollable { + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + + @Override + public int getScrollableUnitIncrement(Rectangle visible, int orientation, int direction) { + return 16; + } + + @Override + public int getScrollableBlockIncrement(Rectangle visible, int orientation, int direction) { + return Math.max(visible.height - 16, 16); + } + + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/TacletCompletionDialog.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/TacletCompletionDialog.java new file mode 100644 index 00000000000..455c377a32c --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/TacletCompletionDialog.java @@ -0,0 +1,382 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.Window; +import java.util.ArrayList; +import java.util.List; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.sequent.FormulaChangeInfo; +import org.key_project.prover.sequent.SequentChangeInfo; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.rusty.Services; +import org.key_project.rusty.logic.op.sv.ModalOperatorSV; +import org.key_project.rusty.logic.op.sv.ProgramSV; +import org.key_project.rusty.logic.op.sv.VariableSV; +import org.key_project.rusty.proof.Goal; +import org.key_project.rusty.proof.io.IntermediateProofReplayer; +import org.key_project.rusty.proof.io.OutputStreamProofSaver; +import org.key_project.rusty.rule.TacletApp; +import org.key_project.rusty.rule.TacletExecutor; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.Nullable; + +/// A modal dialog to complete the open schema-variable instantiations of a taclet application +/// before +/// it is applied. On the left it shows the rule's definition and a typed input field per +/// uninstantiated schema variable; on the right it shows a *live result preview* of the sequents +/// the +/// application would produce (with added/removed/modified formulas diffed). The preview is computed +/// side-effect-free via [TacletExecutor#getResultSequentChanges] (never `Goal.apply`), so it never +/// touches the proof. On apply the completed application is returned (or `null` when cancelled). +final class TacletCompletionDialog extends JDialog { + + private static final Color ADDED = new Color(0x1D, 0x9E, 0x75); + private static final Color REMOVED = new Color(0xC0, 0x39, 0x2B); + + private final TacletApp app; + private final Goal goal; + private final List svs = new ArrayList<>(); + private final List fields = new ArrayList<>(); + private final JLabel status = new JLabel(" "); + private final JButton apply = new JButton("Apply"); + private final JPanel previewBody = new JPanel(); + private final Timer debounce = new Timer(150, e -> validateInputs()); + private @Nullable TacletApp result; + + private TacletCompletionDialog(@Nullable Window owner, TacletApp app, Goal goal) { + super(owner, "Complete taclet: " + app.taclet().name(), ModalityType.APPLICATION_MODAL); + this.app = app; + this.goal = goal; + debounce.setRepeats(false); + + JPanel left = new JPanel(new BorderLayout(0, 10)); + left.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 6)); + left.add(buildRuleSection(), BorderLayout.NORTH); + left.add(buildInstantiateSection(), BorderLayout.CENTER); + + apply.addActionListener(e -> onApply()); + JButton cancel = new JButton("Cancel"); + cancel.addActionListener(e -> dispose()); + status.setForeground(Theme.mutedText()); + JPanel south = new JPanel(new BorderLayout()); + south.setBorder(BorderFactory.createEmptyBorder(8, 12, 12, 12)); + south.add(status, BorderLayout.WEST); + JPanel buttons = new JPanel(); + buttons.setLayout(new BoxLayout(buttons, BoxLayout.LINE_AXIS)); + buttons.add(cancel); + buttons.add(Box.createHorizontalStrut(6)); + buttons.add(apply); + south.add(buttons, BorderLayout.EAST); + + javax.swing.JSplitPane split = new javax.swing.JSplitPane( + javax.swing.JSplitPane.HORIZONTAL_SPLIT, left, buildPreviewSection()); + split.setResizeWeight(0.55); + + add(split, BorderLayout.CENTER); + add(south, BorderLayout.SOUTH); + getRootPane().setDefaultButton(apply); + validateInputs(); + setSize(880, 560); + setLocationRelativeTo(owner); + SwingUtilities.invokeLater(() -> split.setDividerLocation(0.55)); + } + + /// Opens the dialog for `app` (which still has uninstantiated schema variables) and returns the + /// completed application, or `null` if the user cancelled or the input was invalid. + static @Nullable TacletApp completeApp(Component parent, TacletApp app, Goal goal) { + Window owner = SwingUtilities.getWindowAncestor(parent); + TacletCompletionDialog dialog = new TacletCompletionDialog(owner, app, goal); + dialog.setVisible(true); + return dialog.result; + } + + // ── left column ─────────────────────────────────────────────────────────── + + private JPanel buildRuleSection() { + JPanel section = new JPanel(new BorderLayout(0, 4)); + section.add(sectionLabel("Rule: " + app.taclet().name()), BorderLayout.NORTH); + JTextArea body = new JTextArea(app.taclet().toString()); + body.setEditable(false); + body.setFocusable(false); + body.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + body.setMargin(new Insets(4, 6, 4, 6)); + JScrollPane scroll = new JScrollPane(body); + scroll.setPreferredSize(new Dimension(420, 150)); + section.add(scroll, BorderLayout.CENTER); + return section; + } + + private JPanel buildInstantiateSection() { + JPanel section = new JPanel(new BorderLayout(0, 4)); + section.add(sectionLabel("Instantiate schema variables"), BorderLayout.NORTH); + + JPanel form = new JPanel(new GridBagLayout()); + GridBagConstraints g = new GridBagConstraints(); + g.insets = new Insets(3, 4, 3, 4); + g.anchor = GridBagConstraints.WEST; + int row = 0; + DocumentListener live = new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + onEdit(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + onEdit(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + onEdit(); + } + }; + for (SchemaVariable sv : app.uninstantiatedVars()) { + svs.add(sv); + JTextField field = new JTextField(20); + field.getDocument().addDocumentListener(live); + fields.add(field); + g.gridy = row; + g.gridx = 0; + form.add(new JLabel(sv.name() + " (" + kind(sv) + ")"), g); + g.gridx = 1; + g.fill = GridBagConstraints.HORIZONTAL; + g.weightx = 1; + form.add(field, g); + g.fill = GridBagConstraints.NONE; + g.weightx = 0; + row++; + } + JPanel top = new JPanel(new BorderLayout()); + top.add(form, BorderLayout.NORTH); + section.add(new JScrollPane(top), BorderLayout.CENTER); + return section; + } + + // ── right column (preview) ──────────────────────────────────────────────── + + private JPanel buildPreviewSection() { + JPanel section = new JPanel(new BorderLayout(0, 4)); + section.setBorder(BorderFactory.createEmptyBorder(12, 6, 12, 12)); + section.add(sectionLabel("Result preview"), BorderLayout.NORTH); + previewBody.setLayout(new BoxLayout(previewBody, BoxLayout.Y_AXIS)); + JPanel top = new JPanel(new BorderLayout()); + top.add(previewBody, BorderLayout.NORTH); + section.add(new JScrollPane(top), BorderLayout.CENTER); + return section; + } + + /// Single source of truth for the dialog state: parses the current inputs and sets the status + /// line, the Apply button and the preview together. Apply is enabled only when every field + /// parses and the application is complete. + private void validateInputs() { + previewBody.removeAll(); + if (hasEmptyField()) { + setState(Theme.mutedText(), "Enter an instantiation for every schema variable.", false); + previewMessage("Complete the instantiations to preview the result."); + } else { + try { + TacletApp built = parseApp(); + if (!built.complete()) { + setState(Theme.mutedText(), "Some schema variables are still open.", false); + previewMessage("Complete the instantiations to preview the result."); + } else { + setState(Theme.mutedText(), "Ready to apply.", true); + TacletExecutor exec = (TacletExecutor) built.taclet().getExecutor(); + ImmutableList changes = + exec.getResultSequentChanges(goal, built); + if (changes.isEmpty()) { + previewMessage("No preview available for this rule."); + } else { + renderGoals(changes); + } + } + } catch (Exception ex) { + setState(REMOVED, "The instantiation cannot be parsed.", false); + previewMessage("Fix the instantiation to preview the result."); + } + } + previewBody.revalidate(); + previewBody.repaint(); + } + + private boolean hasEmptyField() { + for (JTextField field : fields) { + if (field.getText().trim().isEmpty()) { + return true; + } + } + return false; + } + + private void setState(Color colour, String text, boolean ready) { + status.setForeground(colour); + status.setText(text); + apply.setEnabled(ready); + } + + private void renderGoals(ImmutableList changes) { + Services services = goal.proof().getServices(); + int n = changes.size(); + int i = 1; + for (SequentChangeInfo sci : changes) { + if (i > 1) { + previewBody.add(new JSeparator()); + } + if (n > 1) { + JLabel head = new JLabel("Goal " + i + " of " + n); + head.setForeground(Theme.mutedText()); + head.setBorder(BorderFactory.createEmptyBorder(6, 0, 2, 0)); + previewBody.add(head); + } + renderSide(sci, true, services); + renderSide(sci, false, services); + i++; + } + } + + private void renderSide(SequentChangeInfo sci, boolean antec, Services services) { + ImmutableList removed = sci.removedFormulas(antec); + ImmutableList added = sci.addedFormulas(antec); + ImmutableList modified = sci.modifiedFormulas(antec); + if (removed.isEmpty() && added.isEmpty() && modified.isEmpty()) { + return; + } + JLabel head = new JLabel(antec ? "antecedent" : "succedent"); + head.setForeground(Theme.mutedText()); + head.setBorder(BorderFactory.createEmptyBorder(4, 0, 1, 0)); + previewBody.add(head); + for (SequentFormula f : removed) { + previewBody.add(changeRow("−", REMOVED, f.formula(), services)); + } + for (FormulaChangeInfo m : modified) { + previewBody.add(changeRow("−", REMOVED, m.getOriginalFormula().formula(), services)); + previewBody.add(changeRow("+", ADDED, m.newFormula().formula(), services)); + } + for (SequentFormula f : added) { + previewBody.add(changeRow("+", ADDED, f.formula(), services)); + } + } + + private JComponent changeRow(String marker, Color color, + org.key_project.logic.Term formula, Services services) { + JPanel p = new JPanel(new BorderLayout(6, 0)); + p.setOpaque(false); + JLabel m = new JLabel(marker); + m.setForeground(color); + m.setFont(m.getFont().deriveFont(Font.BOLD)); + m.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 0)); + p.add(m, BorderLayout.WEST); + String printed = OutputStreamProofSaver.printTerm(formula, services).replace('\n', ' '); + JLabel term = new JLabel(printed); + term.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + term.setToolTipText(printed); + p.add(term, BorderLayout.CENTER); + return p; + } + + private void previewMessage(String text) { + JLabel l = new JLabel(text); + l.setForeground(Theme.mutedText()); + previewBody.add(l); + } + + // ── shared ──────────────────────────────────────────────────────────────── + + private JLabel sectionLabel(String text) { + JLabel label = new JLabel(text); + label.setFont(label.getFont().deriveFont(Font.BOLD)); + return label; + } + + private static String kind(SchemaVariable sv) { + if (sv.isFormula()) { + return "formula"; + } + if (sv.isVariable()) { + return "variable"; + } + if (sv.isSkolemTerm()) { + return "skolem term"; + } + if (sv instanceof ProgramSV) { + return "program"; + } + if (sv instanceof ModalOperatorSV) { + return "modality"; + } + return "term"; + } + + private void onEdit() { + // Disable Apply until the (debounced) validation confirms the new input parses. + apply.setEnabled(false); + debounce.restart(); + } + + /// Builds the (possibly completed) application from the current field inputs, reusing the + /// proof-replayer's parsing. Throws when an input is missing or cannot be parsed. + private TacletApp parseApp() throws Exception { + Services services = goal.proof().getServices(); + TacletApp current = app; + for (int i = 0; i < svs.size(); i++) { + if (svs.get(i) instanceof VariableSV vsv) { + current = IntermediateProofReplayer.parseSV1(current, vsv, + fields.get(i).getText().trim(), services); + } + } + for (int i = 0; i < svs.size(); i++) { + if (!(svs.get(i) instanceof VariableSV)) { + current = IntermediateProofReplayer.parseSV2(current, svs.get(i), + fields.get(i).getText().trim(), goal); + } + } + return current; + } + + private void onApply() { + try { + TacletApp completed = parseApp(); + if (!completed.complete()) { + status.setForeground(REMOVED); + status.setText("The taclet is still not completely instantiated."); + return; + } + result = completed; + dispose(); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, + "Could not complete the taclet:\n" + ex.getMessage(), "Invalid instantiation", + JOptionPane.ERROR_MESSAGE); + } + } +} diff --git a/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/Theme.java b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/Theme.java new file mode 100644 index 00000000000..0f0ce00bbac --- /dev/null +++ b/keyext.rusty.gui/src/main/java/org/key_project/rusty/gui/Theme.java @@ -0,0 +1,59 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.awt.Color; +import javax.swing.UIManager; + +/// Look-and-feel-derived colours for the GUI chrome, so the docked layout adapts to the active +/// theme (light or dark) instead of hardcoding a light palette. Each accessor falls back to a light +/// default when the look and feel does not define the key. +final class Theme { + + private Theme() {} + + /// Surface of the tool-window title bars and the sequent header: a shade nudged away from the + /// plain panel background, so a header reads as a distinct band rather than melding into the + /// flat panels and the split-divider arrows next to it. + static Color surface() { + return shade(color("Panel.background", new Color(0xEC, 0xEC, 0xEC))); + } + + /// The hairline separating a title bar / header from its content. + static Color hairline() { + return color("Separator.foreground", color("controlShadow", new Color(0xCC, 0xCC, 0xCC))); + } + + /// Muted text for title-bar labels. + static Color mutedText() { + return color("Label.disabledForeground", new Color(0x55, 0x55, 0x55)); + } + + /// Selection-style highlight for the hovered/targeted sequent term. + static Color selection() { + return color("textHighlight", new Color(0xBB, 0xD6, 0xFB)); + } + + /// Accent colour, used to mark the live (selection-following) editor tab. + static Color accent() { + return color("Component.accentColor", new Color(0x24, 0x75, 0xBF)); + } + + private static Color color(String key, Color fallback) { + Color c = UIManager.getColor(key); + return c != null ? c : fallback; + } + + /// Nudges a colour toward higher contrast with itself: darker in a light theme, lighter in a + /// dark one, so a header band stands slightly apart from the surrounding panels. + private static Color shade(Color c) { + boolean dark = c.getRed() + c.getGreen() + c.getBlue() < 384; + double f = dark ? 1.16 : 0.92; + return new Color(clamp(c.getRed() * f), clamp(c.getGreen() * f), clamp(c.getBlue() * f)); + } + + private static int clamp(double v) { + return Math.max(0, Math.min(255, (int) Math.round(v))); + } +} diff --git a/keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/autoModeStart.png b/keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/autoModeStart.png new file mode 100644 index 0000000000000000000000000000000000000000..34bca9d799e92a1ea275c8f0bdd3fe9e9827ef79 GIT binary patch literal 8064 zcmWkz1yoc`6n@LHu=FAwE+C3@r_|CZA)V4GNQuCLEFmD^(jnlI($d`}BHf^bbP0mA zNc{WHIrHv2@4dO_-tT^Q?##S*I$A0ug!F^}0FbDuDn7%?JZvw3;$g3pWoQ2jJR1!a zMXbcOzY49vSV7>SYT}I@G53E5$+KtrVISiAs6AD}Uk1Slp_GOsUfB0cBx;Is`hGtS zKKlDn8qD_pEVZ2c4kmaAlkdqJptjTXG>3hKcrIEYbn-|E>GVAqee9GwwUw2~c6d;T2($ek2N3H z8uWe$-pM`r_b=4Eh_TBCFk_}_eXp6U@UNt0QW?*Vn2re)AJ)BSK{|B^YD)KOuK$`)PCU?<%hwlIezlxv>ns=dvKmM3wY~m5e zSars`$2w8T`vI7@Sz2e-Q!J(fJd7W`}+ohFp`4<-RxeIHTh1-0D6)4_#S+yV4S9e zooWhTgKbknFzJ|)`dr<_$(=j{Y~q#exA*myrYng}c}qqW@@EQ50ZpB5DH{*()F%Gm zUkb^E+h9M2XSz~h#1Y+8jZfw-#Uj4@280 za4fGY#6zgS=F%-^m&*+Z7&!YX9)XBOXM9{X9sVH=@$SrapFdCf^|}WO#}#|;m#`85 z9w8hYL{QD8ej}h^$vMQGiogXL=fgQ)KXN$0gt{4+ku0RSbLVe;yaCE_j&KU@bFim| z3WXAi2H)3U=G?kRUv;%}($wn5xz{$Tx)o;&x>h((RO{Q$fpR{hr53=;vmxmSSZv)k z9R6+w=u)cvU0;dx;%HuR17zY)R0rElL3Y?k1z;n#MhE{0Q&b-=JDbk&xUl1vx_sQ* z@ciZ+UtGhSve`t;-|mJQmC*P539#`HI|AEZ5GT?8iTZKil=Kp26SEwSRO+op~2z~N;flBXU0Ic|?nv#3iKhvj^> zUdS1hhwd=t__^vDOVM2%-n_S7fNeN1h4PB&iQ-!QwdD>io_E%!a`>GQUjNmQzG3O_ zj`p;dHuXb;OFW=gb1UvUckq5ZbHwvJ@0h3arhj>V^J1boUpH5aeHt2x0QrG^n&~_o zxBNLA&gC(3TsTnpgI>Gzz)ogoFh z=N`7bS`+C#|M&cf)(Dd~OG z9BL)B<0bU!cK+jL+yQZ)Qwf2&Ja}uroy7Z`ns#u+kaPixHIbG2T&$%WvbE{Jh!UG!TY$DK#=1daF>u{X z**DM5$o?n4?983h{BptKi}oy8hs*&c+W5Qd3N?FH2$)Jb#05J4gTkyD8|@!el~oO zn<8;>|0%GY;U^|g%YrDv8mHg%rZO;zTda)##UZz+^%hc(BW*O&zu83d1++`b29~tp z@Y-f{8OQJK3VseB-Kn5~d%s*_rW?eNu-GjSj72-6`ihTggPLydWN?4ss3Cds7; zfLJlFWYZzrD9CPJ2DkY;J>Zz>_wu8Q0=5`K5gma(bXf8c7B!|~V|yjJEB;|ZytdWE z$maEvkqAI{fywi1_0zoX)jISR@~6scVgeJOM25SQt^55;D*8*9VRsK*+D-op+UWZy zq=_{kV2Js1uV`&^F$Kb2MJl&!L@t(1=d*L~Gvzgm%!#U7ZNHD)O@gxe)IC5ObUCbO z^kjpeQkP`Jm$%}Jks}HQ_|CY0UHYO|UAD#0+rYNr5X5}Ia7$S5akO~!Q!f_Hc!|8X zwRO8jT`*q1>NX$pt$iNU&>Uirsq&|Agx63f%BSX6@3?H6w9f6kwJ7e2DqlLI8LZa83qTal<{c0ZRIfgN- zs$a9{ZTNX@nV2b2L;P#OxyEr3YL)!%cik@%&<~5qANR6mcjDNi=Y70*uy!Dnrp6+& zjsDob|GqRbJTUUZ?Uq(LZXnk{ZY}@lxi~Wt!UtapmW40Krg4igT+BKRs>HsnYyCji zY|^>_V|LlhG(ulf{hSVz!-+ojT1cYX3<(e!Am_;UH2NJ>H#yE4&oyJnUFVWNTtlChKY1xFb#3a1o0ibxhJuraVADYS&I11@2P zpIzDN&y^dC$GD4c`FA`$?ra!%?2ozlS;-!49Jr%9eDmgsRx_8{CgxK%Po9rGJ8I5a z%YC+x0Y{NUk?8Ad<1SMM)3}}PYw$&W@hM&J5=Y?$h6J1FH09!vIH{)Y$}@ru->|eH z8VQy(4S(r+NAYA}NBo`?Q=9e8ZC^P~tXgOEv~Sl^yT=-nL2_Z_=Jg+N(voxhiOTCD zd;#-;OVQ)fQ1%A;D$&u1f5&jS!|;xTQKrsNpY*LbwiZtLdp7zFvsl7>W;L~%xgo@* zIg8P+TzOI0$sP=>o(hehRCN(z?ORJ!`Ewc)sjHHl`E(%=ys69XRjZUUG-4X0fMO5m zqdBrTm}5cKpxMz!(U>Uz`P^@;9x9{KD-yOp1sv9O_AkL$X55>E$2&avg{Na+^kZk= zgSo?ZUul0DH9sL2LtRdoBlXqVWvrE-^MIzn)$#d9^p6{ou-Y?E0#@q&I}N ztV`+Mq@so(p*ItvAmg(V745=I1k+#~A5T zc1w1P7k;#fYqXk-{T*~$8x9CuQQW9fmBsZ%a#8bsF-nX3kYKapYTjl@vJgy>RetH9l)pYr+gc9~-5YD=~jI~N61v47uO z+~*$mZC?dGnlimNA#G)|F)d%%ngV%+FZWjO(XX6~SG--=QIO+6CMQFA&~ z*#6_cLOzCg486J`H~2aFbT20FT1++D|9AGk)gmKpos3klG0usXP-T|vYy(FWa?e+p zQm_MDelyuqhF@-UdMS4j4gcRN>dciX(9`gh&WKqvJe;W>Gf(d^Ne|b`4+*>%;Sa?0 zj=yIAQfFv)1J+#jOa6@tdjvlI9^1njkOA7pIz$oVF$C8d2kDbkv*W@6-EPgABCE~u z_p0@D*=DEb@9LXw^62UW((8JT=m07<_&b^|a!7x1NYm;i*N9i&Y$!UdlEw`Mfx;A- z7DE|i{IDo7LvgUT?$reFnbj1Ru2|(^Q^flfV%$RdL3cEFIsE+f?UfQr>rY1#;8!^ zY$Ord$&dd^2kS{o=Kp5Ghu_@8Uuo{HjQ`zwSjVj&sH@=ILe^ygaK4MHE5Pm_C=3#^ zM>dAl%@EsE(zpDj*JiMXeEFoGu2^MdtSMb>2{H9L!B~0G? z&i%Pj%D9t^qA{+!4)eCDX{O)iTaISxUZA{KsPi z5BW)QahZ>B%Ckt~vfC*gACs^xIqsnpq3)CV8!`Y4=*UK0I%w4?|G5V0v&%GU?`8rLsz;{DnY>+O8H@i35FdHi@oJUCm!1;Uiv z_-A2Qiq7g(s3WdL6~u)6{%&uf*<)hdGI)UtLP=K`Qh4Q$N4UyIY!Ro(b%6Vv!hZ#% zI?2CxWkudqlINzOO|?N8?e@f_!NtXq@{)bUId>|IhjY+FD>%8bs}y2Drh{JB45Nb% zdM>&5CGYPRRAj=2FnC9ds73_M(AC8Z#{kC*vP}kCfa9p7drE%Ja<+HpJj|7pgUk#e ze%3Wjv>BkowvbqDH?I6l))BjupL-dYHaU&2R&ryc`foj)Dq?|AaWfZk{KjE5KXmQ| zGsT*_Z2?It)O%p$YPkLsLzrb6w;QRq;gef;Y{<=2S79F_c`fCU$rwSV#7W#J5oLN< z6f}^48Vj}{@mNJ?skNq*D0gKI{E{z)=p$tLJOU z-#)x2FQJLQ^83EG2*0>g+(Q&lMbw2e zI(Ya1iL6|y$GY+0e#F9C6=EuRRz6h-|NJmoB=+lSiUw!*w6F_*y-uM?tBuVVj!)ds zgP`SsqOqP^8nkZ4)uu^S3QZIcO(5Y}lv@6Ln$ueR1#BJZ6kSPzvuKbYkr76i+t4Qu z;)ew)uRt%G zZz!*|Xid?$aV842ua^~`&L@c_w&{Ocf^z7X(OZv+QrQs6bzx!W z8HDME#y5y_uZ^{<;Y{+ExgU*ZZ`OpH&`GI;hxkW!{X5Bg-y}Y3P*T*aYuyj-JoYhz z3&YPCCt}5f>C?jrt#i($R^wKOyM`uTSmh<#*G&ZtIuhiOam)(Gj%A^Sn=l2AA`ZPe z`{^n_+Ri9ZoqZQkNG<^}bQ?ijQU9DyZDq&x&K9BAk9o}y+PV6_@G?9@|Da2uVzlQ9-2;ynm}M0~=iwER9iUq;V#1|)5aGY{3)e07|T z|97y`j{bdKgWVm?=6^Jf6jzEQvz|yM4>#p2#;3wx)|uhK<#ZZCkgCHYG^Q#ae#Em9 zB)ewp()+$HRD8@GmB;SI_pMp}-y z^XSdfoG*9Mzz%uZ`=M5g7&}+OlHefsZ}OXI4~u+pyYw7K21HrMa2!|ZUzDUCS32J8 zxW^qnc7Z1lRgIv5ZrKT@a(($T_5{WyjVe=-g}25Vw;u86&q-cN_7jO8HTvx7jB{95HIBf+s7WBrQ2J@TyINFn9|b5a};3dh|3+6Ek;sB&&fC1ReTn$%+GoB4)!o8$DyXIj0c?N7^@P^ z^WNvKH+v^8tSnA11CD>x_jd>{eGuHvn@xuJQY7}fLJ7ZClujfwL>;8`4{2HnNq7m$ z_u?Up-AkyLWpL3@5*3g^`e-jJyd z=zm))sYpv2JiWLyv46*Ku!V0GZ8Y6ed>)+;L5`^9=W&W2x$UXL8%RBqC-2qc>l&yd z`gY;^3{$^RL()6wL%{Qr7&k}5gJ?5bdwJ@VgK$EZrxZSc&&OS*Or;EY_{cHY{fT(I zj4;_0EF|kvrs=`DX`cyW0rDwRiu}jhMFOjCdxnSu(RJKF_wG5dv-0T8zrS232x5uX zP~K({xqGL22YAY|9@wq(h=KTR_D~o-nbZj3@(8uObB<^N|!47qrT1#il9C3fe z-vWB?M<~Y%KksRW${m46^`(-#r5)ZI()UG)8%tCu4H%>%!TDq|oexxWMevW&ppazg z0-OR&TAinF>BNhzliygSXz=i;OhvyApEnmS^HYJS z-eClf@5I8MQq`nPRKk-b&{B)j>(n(;Wni1BPf_!%TMhH+Gp}bqx8KO#=c;^k(6k4x;>BWDL+5=cELnPH{{v}Axj{L5 zOZ5xJrHtqK;!1?4!|{BP@|BYzQYGKxKC$YfJH6)$CJf~xtYZjgl{bS>>|8o;^Rpki z5t0q^bGiLWmXaPsH}hL>Cb)U)lNM9NLI=8-Ly#zI|2{csJhQ(VWOx}ZlRkYyT5zvu za}<;CKFLhYzBCM=rgSsA%7($8vZ8A6uWp(N1Y9PPLUxBCT=`DHO{C{2g*j3A}CSQ42}-LQGVoop_+nNY7j*tT+K-_2j|!Qg5lsM63Zd8TwFF<~wS zG}oDmH1iD0!r{^>o(L}c$=bj5iseq%_e-@MkR!W^hEGTZKjQbmAdwd#1Kk@paTiiN zI~_ibhQ;TSw8`H8WFjzV%YxhV6h|toYiiKOksc2uNOzjiFdz69HkU0XdXyNTqje~P zzkSbWXz6l9>C*3*RDB8$;NlGm+HuuGE>T54wzY0*N3lnR20zgzDgZ+EADv-2fk87~ z@0P#axJ^%R{NnhP$N&7jl07%^>w9tRQ8LiHy=4E2zjCuKd}?nK*Cn{=n-ZmXRK(W| zst-53G(Dk>gFPOtc)ArA9E3Cw_4+hn@Mbo?R3yAl>gd`YTy^-JHMuYaz3Y>ej7Z~{VHm*j-X4U zH+#R+W$RI`1Y30VOz*7`v%3+n6fls)hwnMgHm^PAQst7 zXJhPJNSTjPK1&u|MsF(YL^5Q=JKnHwhdht;)5AI^4pSn}WB!Qw%@o%|J2=#0`M#&E zxgf5|?gq9Wc==d;iW7*T4m=)6t!Qx19XK$d{awt=$zo_Cuo4E`UHLyntVQFlO z-?8lSoyV)FC41{8p-2b&sz~i$!42nD%!c zGbh6<^hg^$**P-nRI(So8>8?`>sbao>7+|rejiTMTaB~UhnYf1BOE5rt1nmP3LbTx z0nzUZ?M_G6c<*(vJ z$PsTvg_LW&xz{#onC4i`Ka7}eJi$ma$Z|`nI|tx&O`XWT+3AICVu28XnPt4Q(9ZVF*ra2Rk4QNtJv_V9xR@|44m#8E{t4Lmpev2ixm=^?Z=Z@I>B3O=N80!1dY4$Kia zW2vVndZ|*?8-Hr}EA;q~a%9LLV#V6?=c=3OLDuHItYG6YO9pVY1l^>Z$O9CD==~6g z8-q;{v!3)+3Qq?L){u5VJR72<)hhx-@JL(+&?bm=I;N5AubViSfI5OM$_~lcMF>QX z{_5>pyi|&r5-@lB}Ev1rzfHZ<%I))Mg3W6Zr2nrI?y-^AR0@B?rEsSQ;(kh(;rMo9= z>^FFxf9~tv^ZMlbocoD$PQ(jMRSMF(qyPX=s6AEE0RTMQD}W*<#69u5C5r%n9Z*wJ z)b*a-&Gbt!(M!ZCT)t9;pd+M3`UjVvQ-`oW1T9QtLUbn7z)I9$0!ABKcbFl*q$sJW zo~{QzX$K8jO3_6mJPHEgCicbz4DAv+j z>bv?n;ZOtfa9RjBzshPGT{vg91izSIfz=?sBk&N52q(m6L|>5O!>ElKLT_4c3U3>{4g_}te8<&^Cr?jY38m?^BM=SfY3IeH*uurgMP3<_rF(IJrxF|rgP4l)oIP}WT4 zBJYwU$JtdPqmQq^5J%4`1eE!5yWmYs_;-oi?Q%uB(xg7*z)5TYF;40Pc8REb*hCa^ z)RtM|#)ct;Pt!3J*4?~#nH;qxd$^%Kd@1Cy?YV_$;eXIEX|$Yd_5#frfVqD_f{YLL z*ZH|?kKI;mD6J)^Qy>>r2DrY2%}i}RhQdD;KJuAhLW*py#C1kugxT| zvaDBRR{=wdoLbVUEahd^1pDktR0XpRHGcR)ZMO_Q4A6eUri2R!CX#9FMnp;S+jF?& zyWy1(Kv|~;cjmn(znU3KihFE63I43VOsq{w*g!I-Wm4YRl{a7BFX!cH?%m%o=NMnl zGv$(n=NdwfcgN;{jmpGQ3~HU4u=A8OLYQopG77Ji8spFAtrVB1le+M7k8h6F@eTJu z+xG-~(Qu@5QS+}i|3Nu_T_nxZDRl^mD3`OTDuaTibloZ2CF;hrvmLaW?>T_%oZni$ zXrM;7fW)dNu9|->&K`0-df%MKgD2C2Ou`a1QlRuoO$@A9Ur4AUQq%pE8@cdMR6BA| zjP0e(*<7B#YS(aU z_7EPBd$Z=!>1wgn+s-nb06goH-+AAeFo{;gmp%wkY*%c2b*OrmU=77OleO51Z~NKO ztv)=XO&PBw;FrwO&VTc|9e>11Og;?oGN8ITiJT7A&^g5!ej`H<1-JuIFIS?aiUcnx zWEMvK*_Qm6>kXIiSY66gUmNnJUdE+b8@!XpFWLPoHoEgIu`bYI&09B6-Fo)@pMl6v zXg4n@?2_%L3ag_F8L|Kaz}9Ncn<7&H&(*<_^We{SSy(YAn=g|Qcv=Oml(z%W9{HX3p8h17g|kN+f{ zx1nE@M(O}Vp5yuEBLSBEjh1b@@Bo^cr~_0rb@fU|F4K0^7pwWi*Ssa%)vJu_nC1AF zplCD0@dPj4!K_Fy396gEiuT&@#`=P5a|58J?oBy zd-xMBwL~HVZea*t5Sp$;!cQuHg{g|Kgrs?F6FQ~1x~~ssU05qp5`95Xeiw}vaI!zr zOsNw&|Kf9?Vkp2dtRqu$ENmMSE!HF@Gjfh{gy>w6R)bS2wnq2&_L~(%wmAe@%;^rf zLFR;%Jhzu*S~vbND>!Q}%OC%ZJ|d}^TpFIdjHgQ_@P7w}exN!jbNTJ)Ws2Od?&C(33v;?IYipIrRoYvUJw*RWy1Nk}{@_i5KmJl9|H&qn}n=9UE&4 z?S7?UsI*7yA%@lVIV&;7)r}6XO&nP+7wlyZ?Znwp*^G%14Cs*jmMRy@tn32c&gXJ7 z^}P4IdS@nUMu1;GhJ+zS6aPfv2?JqO#sb7aBVq#h;reO+s5~gI zyy)fnf}!3C7t&fPC!n$_^=&aS4!eT*8H2OVZc+S{Uug;@d5M=nF_W9Os!&_do>wRq zQcpK-!K`_5|1!;F1Hl2tKVu(hF3EH964SRB(HA9C36k@{ONY&fyb+zwsrLv@kLGSq zzV1p1cF#MIW-hPS6)g3vCrJZg;$&0O3SBmy_e63vPZ4^B?VU~B0F8}~MjuU)aKOyDzLj53MH>&JmH1CZBu_G12A znU+0*5EU(3w?86ZanSh3 z5?=1@nwkjQD{ia*qw%THsrO&q7fIXkk|O78vV4{Q0yNZ^@zFCC{ex4#f9x!C1NT%8 zrQf$)M#T(B_zg7CGx&bbEzU5`<>0}?7t*i^4JLs+Rjao(exajDpnSTb0}ZSUGdbAU z8ywZwgX5JK=v2%c;)EzAM6yg_&fU+w7zD;a0w7E#L6{%&rS=Gz-R7D7U?5(~hD*Oh z&#@Mqnk?u*Kl^t&<;RRzP20XSAat`=w_{uKv?FJH?zA;b|A6<@Q?9CvQBspU zs5&NWXbT*aPdG`T-)D7;#>K6Wop0-h{ERP@HdDEpe3#_;-$?xsqGSxBkjP z!dUWk36=QqOT0(g2hZgyYsVsV?vALN{lmWK603%;Ac2Uu9I|KCjD^#WN~idE;=Cl3A@4x1>qSNwEU)-rv#XhMd3PkL zZBJ)GB0KpR^kxwb_X~_&d1asHMiDj;nCYR~zBF>ys^;)kHA;$9wlw~Q>0T}mJ_l67 zt$8p9Ml{SBt3u8_f(LWPZsyt({M5}qDMa&Lh7Hhf+MH2p2b$~%pD1swDK7>u?}uae zlZVYR?Wy+v<>m_G-G5jeogU}zIAn|A*E^-O=nzEp>FaK2z-{-RbGeyEe+C+T7Hgfx z#Vy63Y={9>ovQvsr5TNKdg9aHm(p6uf3lH3d@cWY46DggC;Z7Zq*&T|FK?u{lL_0p zy`sci6%msHL=^xfm4~lUq)3fMw!b+c9ToW$r+asLsar~2ES3KfZQ3+2{+y#}0!y@i2TW9O2x+?BHG4yzmzf`riE z5zxfAi;^8s9`N<^=V0?U%)y2f18=eL%=@s?#XLf82f!7PZONOAu2R*Wwf(> z!P5DXp=qRX38#+~DMbTlx8L?jJ}n$vnT(q&J;4R5@5QtijrqB~$_sBbF#pU!UWO>j z^m@0w5oS9OHQ?(@M)e|qUeZKftcgoZ`iSF&5HtRX5jj<4shZK+E0L-EgeThYU*}-_i0tZCfpz4=I9Ux>TQjhGft#O zB>>;xHo|(#k9o9~w3uhx9-QYq58R&-_e$8`)K`{bofgc4Z{nYGTo?=gbf0StYwCBV zU!p5a{~4-4wB*JCtms|MbY4}_T^@UNoR7vENq;`*mZ7xa*vN%&40*`8#DwEe@G}M@ueg-ArYiG}q<@jP z*N)+#jRO4{ZDN3;*I}F~*mI={`ot%$Y|wxF)3XSH1DU;&_SDihiTaMi+jii2lQ8z~jdiwjC48 zv9!20=#d*^7noh(w4jgb`zB$Hf+nu5=r!pT!6U}I&UN%_(#)~qoqAWqw455R} zF_I4xhcXWq8P6Y0Irv);7gh-hqtKYu;La8IZXCNt<=i zSx4~S!cYJ?Ee!T@XAood(_Dtt)9#S3u>$VN(&2H*z=_}{haS7c8p*?RYshu@ziO7E~VHERtI#TSF&zAU7|4drLUuKm^zEi zNvpf)r5kPx%#Ii`)-{M4z?T0$z@}*0I*TTYegzXtmfD=X4;r0(i?S*;c_T3;`V0W6 zJN}L}v(U{HI(!YzuXoYA@FMMDV6F#JK7@K$=y5xcJyj3ti>y zDjQBw^c`MDs7U8qe;+BQ&m2u!I+wecd6_RO4Eu5e#*P$vBq`9 zIT^ZS7rP&%GJMN;Mz;4`7X%zY)oS)crUHQcED$f;d){tpl;GDrPgV5D?ySOFl-`n0 zooNqFWv<7^==q;UI`A$@SM*#@(K-L&N29N_s7;my#x|DZm*GnthaUJ6{{Kow<_K7- zLw>6UjP6geCB%fB7QdK`KQX&itZnEg4vN$*lV-nXp!x=fJ4vD=lLsHJ#m# z*bmZ8E_O9lE?<2bNe~uF?D+2Ds9Ispi$6N%b~YpV4qCQUiVsRe6XO*4cEWU;sz3U` zNf1b-od4YL_&8~zobe^}o}w?&u1-GQgQYuO1c6U6cPl;&$P~I-`W?Tch24{UzhW}#aje7;ncbBlvVGRf#kf&N^4pPR_}z3m8~9b^ zI`)_B4A`Qa_sj!>E0=Hc41{n>Mn~f+5Czh&acCvJoQ|De*9GcOBra+8%!f5G;;Q2( zK{Rs|24imcEi6r_{E)MFx;0$Srg>~e3wUuUHo4>>;4W>02gvE(_@LjQx3?2_L+H`I z)?Q5_6S9+FM1a~0p?L_`Lxz)FniP8;@VEe!@+n^}-QVdw*Y&q+rN%`+o>eENB39;U zaZz;MU3oN55d-4eq3tJy_`E2r^y7c`IT<$RkDjw1x@jF>KjWG>~@~77A;A{-9m~ORt!0S|+!NxRn zIXX`yTKPE{Wga%e;cHx4bu4}?X^c<&%J7AdyE@lH!jieDf;^3o?PsFpLSk}gmAH8I z)lSux!LWD%lp_UuH61Phe@!B2%GX8REBSrr=dmHNKTr61C5P4udoJC#ye-pRzoF8? zbDqg7@tD|BJl4kJ}PqI;OxO0RuCcWbzMb`i{uems_p zjlNhE#07Jgb2<41+Ht?kmXc#$M8ve3mBkmI7!t^%D1ob=pFeDoT(t2?FKNqJf-FGP zf!BMIfK<13w44H1XjQ%7%bFZFkh|WfmMp*5KjP4Qc@)G5o3n*F%gpcOR__)}W@Og9 z0Z}`FGdJGkw2RE|Q_A@7Fjg8BDA$N0!0huS$fB$B5OZu5W3#3>lKRnWRvQqtk&a_3 zCa5g~91{8zko7nc9vfG;v!}>-O4p39dysD9xOA%SkWJms>s;(LZOmzF|WhXSSqnRp>ZRFsSkS^!a$FPAEjePZIvBB&4DUeHMBMN-1*)ADtFNJ=3Xh9wu8D3xyV`ok zGnaULA(O1!BXmPYn^FJOQ*~JM<^w|x7%@*Y%h9<1=RhWo7cR6b*pv_3HQng<63#e+ zjPF=Ge&+^$W?|$c_{Z~P6YLfIeg1z?dlb7M`TG8E?78#EwS9RXaoc-}R33Spe8>6Q zjyK|aE};H_(y=Zg<7qn3c9}BI+H0%ca5qTKU6m^kuJN_}NO;GB16ER!28edYV6%+M zu2E9@$UB@G5Y+Y@AiM)_Z{&M_jfutI!=8yfh1SX6#@_5(XE&n4Nnqs*&=fYt^qU!Q z*D<=i-=DhP*Z+UuqjCE-T|rt>KGgZG1n+Co2OE&OZrqMpZ$KhCjb&J@q_gE-YUAzU zx5h3vD<`h&=OuJAB72j$jpBtd@guz3QadNv6=$;7;1EA7O&Zd*U?O@#L3erN)jb?| z*RJRxvmaO z^7d9mmaD#0rRU9jzF29wM4z*k7u-z$yHfrY6@bJ2VpPOr>>Az9C(Tj{gsR0@kuf?y zyy|NhUE*9kFGeE^dzyGwoHTE=8UEy}Y``|0V5=r?NXC#wlXSGO4&U4x(_p#|p0WoI z8Qt{qc;CYfzo}OUqF8up?XTz9Lqm6=&ED60q;p=w_9xZYdJ$=LC4?4sVOfkF*D|fR zUha)0TXi`fn{E%PuB9t0(7|u~x8a7HqN^IEFzX9h?fyRnE);J(K;D7nkLy^!zZiX$ z)_d(i@OIYcS!l)8Qk0C*H44CQEXlt%SO7DH9jKb*h9?#^F$y&ux`OiI&O(OsnEGz` zKIJb!4J?Z0tu}7>5GKEVZYpF=Z+8eX{kS2}#Y))q@$PKc=-gTYTaX!COV25}pW%U( z)4M)95RiUwBc6S;Ls}Z6+VcuasAE%eK%d+cRdJ&DFKU(s}}CZD2Cxj(BQA%goi_EEHZ82yZ}M)b-*o zK9YIY-`ypA@#sKGBK}2>8I~e0wN7}N!GWw&!r2$$VEZw;K!r{1Stm}>eu8g{?{MA| z+#k-=>A-rBH)CmirzE`!JFbbnGxD+;C-Nwd$iQu&_xLQIg~H|(`>NP-lEK6->kPJP z`;qGUl#05#C9)>1TN68DG~YlV@%&Mw6T+3sdwS?@BVrNOqMpp)74BJ`0%S%FHt8IX(cAPd&0@h<2d79ZX++tmSj_ran;Is)d zMD{c7dw_%<`RS^`J-*?wdC_25>E{5O6a!Nlk~bi1&io|z>N>K>vF7FqHRG)Vh1wM zk;|I)c3>zf0JG$K_zw~vlDjCFB(t8H-SmAnc)?+d5X8gBdlrr9-JI5@M5AosBT3MyR9UVjXi#a zJLhTYv1iGO+hEjwfAH0MhQYr+Twr>PfvG%H?;LLpAnNXszV6N{DS?Nb>?md(0Y=tO zKX!$Piazy!qQb4bKkEGau)#ko8&cBmPO1?Th@LU;*pSq$GhHN_8is3{rLPGhFJJt9Yg~I)x+-Nyk^R3Q2&%wvL(s2nh7Oj>c;TZtW_Mf*8MaL zFghL#eL)crB7gUpk`vffdk`$U3?U1fR!6LJcs}8WnFzjm&j)NrAkOo?apzQL5g zgqa6qOVz#zV9rr04OyBv~=&4=zi3*ec=? zjg*~6)x{PEuubxv+=9==Ao!^}2$DX97KktGGxU z^&Ss<7Pbnp%|KJLB4~*$zIQoX2aDivJuC9P{J3-J!CS637bFIY@3fl1g~irl9o_M? zU@I7@o%=a`XD$9@1>%%#afq`TRGmc~g{MiV38)Y)4D43?>(V9q#4ckPu9Wty)MSGk z?hMH|D7{R1wLfa*A`323b*7@b)eDoXm0ct2IL zrN2DlY^?v{W literal 0 HcmV?d00001 diff --git a/keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/open.png b/keyext.rusty.gui/src/main/resources/org/key_project/rusty/gui/icons/open.png new file mode 100644 index 0000000000000000000000000000000000000000..aa697e0212d27e6d79881d1e3cece3c51f516846 GIT binary patch literal 14175 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 getInterestingInstantiations(SVInstantiations inst) { return s; } - private static String printSequent(Sequent val, + public static String printSequent(Sequent val, Services services) { final LogicPrinter printer = createLogicPrinter(services, services == null); printer.printSequent(val); diff --git a/keyext.rusty/src/main/java/org/key_project/rusty/rule/TacletExecutor.java b/keyext.rusty/src/main/java/org/key_project/rusty/rule/TacletExecutor.java index 2f90b937e2e..4cec4774c88 100644 --- a/keyext.rusty/src/main/java/org/key_project/rusty/rule/TacletExecutor.java +++ b/keyext.rusty/src/main/java/org/key_project/rusty/rule/TacletExecutor.java @@ -227,4 +227,13 @@ protected void applyAddProgVars(ImmutableSet getResultSequentChanges(Goal goal, + org.key_project.prover.rules.RuleApp ruleApp) { + return ImmutableSLList.nil(); + } } diff --git a/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/FindTacletExecutor.java b/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/FindTacletExecutor.java index 2627a7a89a0..2c04ca28be6 100644 --- a/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/FindTacletExecutor.java +++ b/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/FindTacletExecutor.java @@ -84,6 +84,36 @@ public ImmutableList apply(Goal goal, org.key_project.prover.rules.RuleApp return newGoals; } + @Override + public org.key_project.util.collection.ImmutableList getResultSequentChanges( + Goal goal, org.key_project.prover.rules.RuleApp ruleApp) { + final var services = goal.getOverlayServices(); + final var tacletApp = (TacletApp) ruleApp; + final MatchConditions mc = tacletApp.matchConditions(); + final var newSequentsForGoals = checkAssumesGoals(goal, + tacletApp.assumesFormulaInstantiations(), mc, taclet.goalTemplates().size()); + org.key_project.util.collection.ImmutableList result = + org.key_project.util.collection.ImmutableSLList.nil(); + final var it = newSequentsForGoals.iterator(); + for (var nextGT : taclet.goalTemplates()) { + final var gt = (TacletGoalTemplate) nextGT; + final SequentChangeInfo currentSequent = it.next(); + // Mirrors apply(...) but never splits the goal or sets its sequent; skips the + // goal-mutating add-rule / add-progvar steps: only builds the would-be sequents. + applyReplacewith(gt, currentSequent, tacletApp.posInOccurrence(), mc, goal, tacletApp, + services); + final PosInOccurrence posWhereToAdd = + updatePositionInformation(tacletApp, gt, currentSequent); + applyAdd(gt.sequent(), currentSequent, posWhereToAdd, tacletApp.posInOccurrence(), mc, + goal, tacletApp, services); + result = result.append(currentSequent); + } + while (it.hasNext()) { + result = result.append(it.next()); + } + return result; + } + /// applies the `add`-expressions of taclet goal descriptions /// /// @param add the [Sequent] with the uninstantiated [SequentFormula]'s to be added diff --git a/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/NoFindTacletExecutor.java b/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/NoFindTacletExecutor.java index 1f8037b5833..f7bb14a3ffa 100644 --- a/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/NoFindTacletExecutor.java +++ b/keyext.rusty/src/main/java/org/key_project/rusty/rule/executor/rustydl/NoFindTacletExecutor.java @@ -63,6 +63,29 @@ public ImmutableList apply(Goal goal, org.key_project.prover.rules.RuleApp return newGoals; } + @Override + public org.key_project.util.collection.ImmutableList getResultSequentChanges( + Goal goal, org.key_project.prover.rules.RuleApp ruleApp) { + final var services = goal.getOverlayServices(); + final var tacletApp = (TacletApp) ruleApp; + final MatchConditions mc = tacletApp.matchConditions(); + final var newSequentsForGoals = checkAssumesGoals(goal, + tacletApp.assumesFormulaInstantiations(), mc, taclet.goalTemplates().size()); + org.key_project.util.collection.ImmutableList result = + org.key_project.util.collection.ImmutableSLList.nil(); + final var it = newSequentsForGoals.iterator(); + for (var nextGT : taclet.goalTemplates()) { + final TacletGoalTemplate gt = (TacletGoalTemplate) nextGT; + final SequentChangeInfo currentSequent = it.next(); + applyAdd(gt.sequent(), currentSequent, services, mc, goal, tacletApp); + result = result.append(currentSequent); + } + while (it.hasNext()) { + result = result.append(it.next()); + } + return result; + } + /// adds the sequent of the add part of the Taclet to the goal sequent /// /// @param add the Sequent to be added diff --git a/settings.gradle b/settings.gradle index 420f02e86da..f6832857fb3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ include 'keyext.slicing' include 'keyext.caching' include 'keyext.isabelletranslation' include 'keyext.rusty' +include 'keyext.rusty.gui' // ENABLE NULLNESS here or on the CLI // This flag is activated to enable the checker framework. From adc3d587331d5cea37179f25c9270dac37a9e9b9 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Wed, 17 Jun 2026 00:02:55 +0200 Subject: [PATCH 2/2] gui: headless smoke test for keyext.rusty.gui Loads if.key, wires ProofContext and the views, runs auto mode and checks the model/views update without error. --- .../key_project/rusty/gui/GuiSmokeTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 keyext.rusty.gui/src/test/java/org/key_project/rusty/gui/GuiSmokeTest.java diff --git a/keyext.rusty.gui/src/test/java/org/key_project/rusty/gui/GuiSmokeTest.java b/keyext.rusty.gui/src/test/java/org/key_project/rusty/gui/GuiSmokeTest.java new file mode 100644 index 00000000000..2760a147e5c --- /dev/null +++ b/keyext.rusty.gui/src/test/java/org/key_project/rusty/gui/GuiSmokeTest.java @@ -0,0 +1,81 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.rusty.gui; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.key_project.rusty.control.KeYEnvironment; +import org.key_project.rusty.proof.Node; +import org.key_project.rusty.proof.Proof; +import org.key_project.rusty.proof.io.OutputStreamProofSaver; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// Headless smoke test: load an example, wire it through the [ProofContext] and the views, run auto +/// mode and check the model/views update without error. Top-level windows are not created (they +/// need +/// a display), but the panels are exercised. +public class GuiSmokeTest { + + static { + System.setProperty("java.awt.headless", "true"); + } + + private static File example() { + Path p = Path.of("keyext.rusty/src/test/resources/testcase/examples/if.key"); + Path q = Files.exists(p) ? p + : Path.of("../keyext.rusty/src/test/resources/testcase/examples/if.key"); + return q.toFile(); + } + + @Test + void loadsWiresAndRefreshesViews() throws Exception { + File file = example(); + assertTrue(file.exists(), "example must exist: " + file.getAbsolutePath()); + + KeYEnvironment env = KeYEnvironment.load(file); + Proof proof = env.getLoadedProof(); + assertNotNull(proof); + + ProofContext context = new ProofContext(); + // Construct the views (registers them as listeners); panels are headless-safe. + ProofTreePanel tree = new ProofTreePanel(context); + GoalsView goals = new GoalsView(context); + SequentView sequent = new SequentView(context); + NodeInfoView info = new NodeInfoView(context); + EditorArea editor = new EditorArea(context, () -> { + }); + StrategyView strategy = new StrategyView(context); + assertNotNull(tree); + assertNotNull(goals); + assertNotNull(sequent); + assertNotNull(info); + assertNotNull(editor); + assertNotNull(strategy); + + context.setProof(env, proof); + assertTrue(context.getSelectedNode() == proof.root(), "root should be selected on load"); + assertFalse(proof.openGoals().isEmpty(), "freshly loaded proof should have an open goal"); + + // The selected node renders to a non-empty sequent. + Node root = proof.root(); + String rendered = + OutputStreamProofSaver.printSequent(root.sequent(), root.proof().getServices()); + assertTrue(rendered.contains("=") || rendered.contains("\\<"), + "root sequent should render: " + rendered); + + // Run auto mode and refresh; it should apply at least one rule (the proof grows). + env.getProofControl().startAndWaitForAutoMode(proof); + context.fireProofChanged(); + assertTrue(proof.countNodes() > 1, "auto mode should apply rules"); + + env.dispose(); + } +}