{
  "id": "app-wizard",
  "kind": "app",
  "tags": [
    "forms",
    "navigation"
  ],
  "title": "Order entry — a wizard",
  "note": "A linear flow that validates per step, lets the reader walk back freely, and ends on a review built from the same data the steps collected.",
  "className": "WizardApp",
  "package": "dev.cf.app.wizard",
  "doc": "<p><b>Order entry</b> — the wizard shape: a linear flow with per-step validation and a review.</p>\n\n<pre>\ngradlew lwjgl3:run --args=\"classname dev.cf.app.wizard.WizardApp\"\n</pre>\n\n<h2>The shape</h2>\n\n<p>One host Window that owns the trail, the footer and the current step; the steps themselves are\nplain objects (<code>WizardStep</code>) that draw into a body the host provides. Not screens, not\nwindows — because the host has to be able to rebuild any of them at any time, in either\ndirection, and a stack of Windows cannot do that.</p>\n\n<h3>The four rules that make a wizard bearable</h3>\n\n<ol>\n  <li><b>All state lives in one object</b> (<code>Order</code>), never in the steps. This is what makes\n      going backwards work at all — see the <code>Order</code> javadoc for what happens otherwise.</li>\n  <li><b>Backwards is always free.</b> <code>back()</code> never validates. Blocking a user from\n      returning to fix something because the step they are on is incomplete is the single most\n      infuriating thing a wizard can do, and it is usually an accident of putting one validate\n      call in the wrong place.</li>\n  <li><b>Forwards validates, and says what and where.</b> <code>problem()</code> returns a\n      sentence, which the footer shows. \"Please complete all fields\" is what a wizard says when\n      nobody decided who owns the rule.</li>\n  <li><b>The trail is navigation, not decoration.</b> Tapping a dot goes there — subject to the\n      same forward rule, so you can always jump back and only jump forward through completed\n      steps. <code>StepTrail</code> draws it; the gating is the app's business, which is why the\n      widget does not attempt it.</li>\n</ol>\n\n<p>What is deliberately missing: saving a half-finished order and resuming it later. It is a real\nrequirement in real order entry, it is a persistence problem rather than a wizard problem, and\nthe seam for it is exactly the <code>Order</code> object — serialise that one thing and you have it.</p>",
  "api": [
    {
      "name": "Alert",
      "doc": "/docs/ui/widgets/alert"
    },
    {
      "name": "Button",
      "doc": "/docs/ui/widgets/button"
    },
    {
      "name": "Card",
      "doc": null
    },
    {
      "name": "Component",
      "doc": null
    },
    {
      "name": "Debug",
      "doc": null
    },
    {
      "name": "Dropdown",
      "doc": "/docs/ui/widgets/dropdown"
    },
    {
      "name": "Gap",
      "doc": null
    },
    {
      "name": "GridLayout",
      "doc": null
    },
    {
      "name": "Group",
      "doc": "/docs/ui/widgets/group"
    },
    {
      "name": "Hint",
      "doc": "/docs/ui/widgets/hint"
    },
    {
      "name": "Label",
      "doc": "/docs/ui/widgets/label"
    },
    {
      "name": "MaterialIcon",
      "doc": null
    },
    {
      "name": "Option",
      "doc": "/docs/ui/widgets/option"
    },
    {
      "name": "Screen",
      "doc": null
    },
    {
      "name": "StepTrail",
      "doc": null
    },
    {
      "name": "Styles",
      "doc": null
    },
    {
      "name": "Switch",
      "doc": "/docs/ui/widgets/switch"
    },
    {
      "name": "TextField",
      "doc": "/docs/ui/widgets/textfield"
    },
    {
      "name": "VerticalLayout",
      "doc": null
    },
    {
      "name": "Widgets",
      "doc": null
    },
    {
      "name": "Window",
      "doc": null
    }
  ],
  "files": [
    {
      "path": "dev/cf/app/wizard/WizardApp.java",
      "name": "WizardApp.java",
      "code": "package dev.cf.app.wizard;\n\nimport dev.cf.Described;\nimport dev.cf.Tones;\nimport dev.cf.app.AppBar;\nimport dev.cf.app.AppTheme;\n\nimport dev.crossfly.ui.*;\nimport dev.crossfly.ui.color.*;\nimport dev.crossfly.ui.graphics.*;\nimport dev.crossfly.ui.layout.*;\nimport dev.crossfly.ui.style.*;\nimport java.util.ArrayList;\nimport java.util.List;\n\nimport static dev.crossfly.ui.Widgets.*;\n\n/**\n * <b>Order entry</b> — the wizard shape: a linear flow with per-step validation and a review.\n *\n * <pre>\n * gradlew lwjgl3:run --args=\"classname dev.cf.app.wizard.WizardApp\"\n * </pre>\n *\n * <h2>The shape</h2>\n *\n * <p>One host Window that owns the trail, the footer and the current step; the steps themselves are\n * plain objects ({@link WizardStep}) that draw into a body the host provides. Not screens, not\n * windows — because the host has to be able to rebuild any of them at any time, in either\n * direction, and a stack of Windows cannot do that.</p>\n *\n * <h3>The four rules that make a wizard bearable</h3>\n *\n * <ol>\n *   <li><b>All state lives in one object</b> ({@link Order}), never in the steps. This is what makes\n *       going backwards work at all — see the {@code Order} javadoc for what happens otherwise.</li>\n *   <li><b>Backwards is always free.</b> {@link #back()} never validates. Blocking a user from\n *       returning to fix something because the step they are on is incomplete is the single most\n *       infuriating thing a wizard can do, and it is usually an accident of putting one validate\n *       call in the wrong place.</li>\n *   <li><b>Forwards validates, and says what and where.</b> {@link WizardStep#problem()} returns a\n *       sentence, which the footer shows. \"Please complete all fields\" is what a wizard says when\n *       nobody decided who owns the rule.</li>\n *   <li><b>The trail is navigation, not decoration.</b> Tapping a dot goes there — subject to the\n *       same forward rule, so you can always jump back and only jump forward through completed\n *       steps. {@link StepTrail} draws it; the gating is the app's business, which is why the\n *       widget does not attempt it.</li>\n * </ol>\n *\n * <p>What is deliberately missing: saving a half-finished order and resuming it later. It is a real\n * requirement in real order entry, it is a persistence problem rather than a wizard problem, and\n * the seam for it is exactly the {@link Order} object — serialise that one thing and you have it.</p>\n */\npublic class WizardApp extends Window implements Described\n{\n   private final Order order = new Order();\n   private final List<WizardStep> steps = new ArrayList<>();\n\n   private int current;\n\n   private StepTrail trail;\n   private Component body;\n   private Label problem;\n   private Button back, next;\n\n   @Override\n   public String title()\n   {\n      return \"Order entry — a wizard\";\n   }\n\n   @Override\n   public String note()\n   {\n      return \"A linear flow that validates per step, lets the reader walk back freely, and ends\"\n         + \" on a review built from the same data the steps collected.\";\n   }\n\n   /** Hands the palette back to whatever was in force before this app opened. */\n   @Override\n   public void onRemove()\n   {\n      super.onRemove();\n      AppTheme.leave();\n   }\n\n   @Override\n   public void onAdd(boolean firstTime) throws Exception\n   {\n      if (!firstTime)\n         return;\n      // The brand FIRST: fullscreenColor below reads a token, and reading it before the palette\n      // is declared bakes in the skin's neutral grey for the life of the window.\n      AppTheme.enter();\n      // The PAGE, not the surface. Cards, buttons and fields are all drawn on the surface, so a\n      // page painted the same colour makes every one of their edges disappear. Read through\n      // onRestyle so a light/dark flip re-derives it instead of keeping the old mode's value.\n      fullscreenColor = AppTheme.pageColor();\n      onRestyle(() -> fullscreenColor = AppTheme.pageColor());\n      setLayout(new VerticalLayout(Position.START, Position.START));\n\n      steps.add(new CustomerStep(order));\n      steps.add(new ItemsStep(order));\n      steps.add(new DeliveryStep(order));\n      // The review is built over the OTHER steps, so it is added holding a list that excludes it.\n      steps.add(new ReviewStep(order, new ArrayList<>(steps), this));\n\n      add(0f, new AppBar(\"New order\", isRoot() ? null : this::hide));\n\n      trail = new StepTrail();\n      for (int i = 0; i < steps.size(); i++)\n      {\n         final int idx = i;\n         WizardStep s = steps.get(i);\n         trail.step(s.title(), s.icon(), () -> goToStep(idx));\n      }\n      trail.testTag(\"wizard.trail\");\n      trail.getStyle()\n         .ingap(Gap.ofFont(1f, .6f, 1f, .6f));\n      add(0f, trail);\n\n      body = new Component();\n      body.setScrollable(ScrollDirection.VERTICAL);\n      body.getStyle()\n         .ingap(Gap.ofFont(1f, 1f, 1f, 1f));\n      body.testTag(\"wizard.body\");\n      add(LAY_FILL, body);\n\n      add(0f, footer());\n      showStep(0);\n   }\n\n   private Component footer() throws Exception\n   {\n      Component f = new Component();\n      f.setLayout(new VerticalLayout(Position.START, Position.START));\n      f.getStyle()\n         .backColor(Styles.backColor)\n         .shadow(0, -.06f, .25f, Styles.borderColor)\n         .ingap(Gap.ofFont(1f, .6f, 1f, .6f));\n\n      // The message sits ABOVE the buttons, next to the thing it is about — not in a dialog. A\n      // validation error in a modal makes the user dismiss it before they can act on it.\n      problem = label(\"\", 1f, false, secondary(), Position.START);\n      problem.testTag(\"wizard.problem\");\n      problem.setVisible(false);\n      f.add(LAY_AUTO, outgap(problem, 0, 0, 0, .5f));\n\n      Component row = new Component();\n      row.setLayout(new GridLayout(Position.START, Position.CENTER));\n\n      back = new Button(\"Back\");\n      back.testTag(\"wizard.back\");\n      back.onAction(c -> back());\n\n      next = Tones.action(\"Next\");\n      next.testTag(\"wizard.next\");\n      next.onAction(c -> next());\n\n      row.add(.5f, .5f, back, next);\n      f.add(LAY_AUTO, row);\n      return f;\n   }\n\n   /**\n    * Jumps to a step.\n    *\n    * <p>Backwards is unconditional. Forwards runs the same gate {@link #next()} does, one step at a\n    * time, so the trail cannot be used to skip a validation that the Next button enforces.</p>\n    *\n    * @param index the step to go to\n    */\n   public void goToStep(int index)\n   {\n      if (index == current)\n         return;\n      if (index < current)\n      {\n         steps.get(current)\n            .save();\n         showStep(index);\n         return;\n      }\n      while (current < index)\n         if (!next())\n            return;\n   }\n\n   /**\n    * Advances one step if the current one is complete.\n    *\n    * @return whether it moved\n    */\n   private boolean next()\n   {\n      WizardStep s = steps.get(current);\n      s.save();\n      String p = s.problem();\n      if (p != null)\n      {\n         say(p);\n         return false;\n      }\n      if (current == steps.size() - 1)\n      {\n         confirm();\n         return false;\n      }\n      showStep(current + 1);\n      return true;\n   }\n\n   /** Goes back one step. Saves on the way out, and never validates. */\n   private void back()\n   {\n      if (current == 0)\n         return;\n      steps.get(current)\n         .save();\n      showStep(current - 1);\n   }\n\n   private void showStep(int index)\n   {\n      try\n      {\n         current = index;\n         say(null);\n\n         body.removeAll();\n         // removeAll() takes the layout with it.\n         body.setLayout(new VerticalLayout(Position.START, Position.START));\n         steps.get(index)\n            .build(body);\n\n         trail.current(index);\n         trail.header((index + 1) + \"/\" + steps.size(), steps.get(index)\n            .title()\n            .toUpperCase(), \"Total\", Order.money(order.totalCents()));\n\n         back.setEnabled(index > 0);\n         next.setText(index == steps.size() - 1 ? \"Place order\" : \"Next\");\n\n         setValidLayout(false);\n         Screen.repaint();\n      }\n      catch (Exception e)\n      {\n         dev.crossfly.util.Debug.handle(e);\n      }\n   }\n\n   private void say(String message)\n   {\n      problem.setText(message == null ? \"\" : message);\n      problem.setVisible(message != null);\n      problem.setValidLayout(false);\n      if (message != null)\n         Screen.repaint();\n   }\n\n   /**\n    * The end of the flow.\n    *\n    * <p>It asks once, naming the total, and it is the only confirmation in the whole wizard. A\n    * wizard that confirms each step teaches the user to dismiss confirmations, which is exactly the\n    * habit you do not want them to have when they reach this one.</p>\n    */\n   private void confirm()\n   {\n      Alert a = new Alert(\"Place this order?\",\n         \"Total \" + Order.money(order.totalCents()) + \" for \" + order.customer + \".\", \"Cancel\",\n         \"Place order\");\n      a.buttonAction(1, c ->\n      {\n         Hint.show(\"Order placed\", 2500);\n         hide();\n      });\n      a.show();\n   }\n}\n"
    },
    {
      "path": "dev/cf/app/wizard/WizardStep.java",
      "name": "WizardStep.java",
      "code": "package dev.cf.app.wizard;\n\nimport dev.crossfly.ui.Component;\n\n/**\n * One step of the wizard.\n *\n * <p>The contract is four methods, and each of them exists because of a mistake that is easy to\n * make without it.</p>\n *\n * <ul>\n *   <li>{@link #build(Component)} — draw the step. Called every time the step is entered, forwards\n *       or backwards, so it must read its values from the {@link Order} rather than assume it is\n *       being built for the first time.</li>\n *   <li>{@link #save()} — copy what is on screen into the order. Called when the user leaves the\n *       step in <b>either</b> direction. Saving only on Next is how a wizard loses an edit the user\n *       made just before pressing Back.</li>\n *   <li>{@link #problem()} — say what is wrong, in words, or null when the step is complete.\n *       Returning a boolean instead is what produces \"Please complete this step\".</li>\n *   <li>{@link #summary()} — the lines the review page shows for this step. It lives here because\n *       the step is the only thing that knows which of its fields matter; a review page that\n *       reaches into the order and formats everything itself has to be edited every time any step\n *       changes.</li>\n * </ul>\n */\npublic abstract class WizardStep\n{\n   /** The order every step reads from and writes to. */\n   protected final Order order;\n\n   /**\n    * @param order the shared order\n    */\n   protected WizardStep(Order order)\n   {\n      this.order = order;\n   }\n\n   /** The step's name, shown on the trail and as the heading. */\n   public abstract String title();\n\n   /** The Material Icons codepoint for this step's dot on the trail. */\n   public abstract int icon();\n\n   /** A stable prefix for this step's test tags. */\n   public abstract String tag();\n\n   /**\n    * Draws the step.\n    *\n    * @param body a vertical, scrollable container to add to\n    * @throws Exception if building fails\n    */\n   public abstract void build(Component body) throws Exception;\n\n   /** Copies the on-screen values into the order. Called on the way out, in both directions. */\n   public abstract void save();\n\n   /**\n    * What is stopping this step from being complete.\n    *\n    * @return the message to show, or null when the step is in order\n    */\n   public abstract String problem();\n\n   /**\n    * The label/value pairs the review page shows for this step.\n    *\n    * @return an even-length array, {@code label, value, label, value…}\n    */\n   public abstract String[] summary();\n}\n"
    },
    {
      "path": "dev/cf/app/wizard/Order.java",
      "name": "Order.java",
      "code": "package dev.cf.app.wizard;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * What the wizard is collecting: one object, filled in over four steps.\n *\n * <p><b>The single most important decision in a wizard shape.</b> The steps write into this and\n * read from it; no step holds state of its own that survives its own screen. That is what makes\n * going backwards work — a step rebuilt on the way back reads what it wrote and shows it — and it\n * is what makes the review page possible at all, because the review has one place to read from\n * rather than four screens to interrogate.</p>\n *\n * <p>The alternative, where each step keeps its own fields and hands them forward, works right up\n * until the user goes back two steps and changes something. Then the forward hand-off has already\n * happened, and half the wizard is holding stale copies.</p>\n */\npublic class Order\n{\n   /** One line of the order. */\n   public static class Line\n   {\n      /** What was ordered. */\n      public final String product;\n      /** How many. */\n      public int quantity;\n      /** Unit price in cents. */\n      public final long unitCents;\n\n      /**\n       * @param product   the product name\n       * @param quantity  how many\n       * @param unitCents the unit price in cents\n       */\n      public Line(String product, int quantity, long unitCents)\n      {\n         this.product = product;\n         this.quantity = quantity;\n         this.unitCents = unitCents;\n      }\n\n      /** This line's total, in cents. */\n      public long totalCents()\n      {\n         return quantity * unitCents;\n      }\n   }\n\n   // ── Step 1: who ──\n   /** The customer's name. */\n   public String customer = \"\";\n   /** Contact e-mail. */\n   public String email = \"\";\n   /** Purchase-order reference the customer gave, if any. */\n   public String reference = \"\";\n\n   // ── Step 2: what ──\n   /** The order lines. */\n   public final List<Line> lines = new ArrayList<>();\n\n   // ── Step 3: how ──\n   /** Index into {@link #SHIPPING}. */\n   public int shipping = -1;\n   /** Delivery address. */\n   public String address = \"\";\n   /** Whether the customer asked for a delivery note. */\n   public boolean deliveryNote = true;\n\n   /** The shipping options, and their cost in cents. */\n   public static final String[] SHIPPING = { \"Standard — 5 working days\", \"Express — next day\",\n      \"Collection — customer picks up\" };\n\n   /** Cost of each {@link #SHIPPING} option, in cents. */\n   public static final long[] SHIPPING_COST = { 0, 2450, 0 };\n\n   /** The catalogue the items step offers. */\n   public static final String[] PRODUCT = { \"Ball bearing 6204\", \"Oil seal 35x52\", \"Relay 24V\",\n      \"Hydraulic hose 1/2\", \"Filter cartridge 10µ\" };\n\n   /** Unit price of each {@link #PRODUCT}, in cents. */\n   public static final long[] PRICE = { 1240, 890, 3150, 2270, 1680 };\n\n   /** The sum of the lines, before shipping. */\n   public long subtotalCents()\n   {\n      long t = 0;\n      for (Line l : lines)\n         t += l.totalCents();\n      return t;\n   }\n\n   /** What shipping costs, or 0 when nothing has been chosen yet. */\n   public long shippingCents()\n   {\n      return shipping < 0 ? 0 : SHIPPING_COST[shipping];\n   }\n\n   /** The figure the review page shows and the confirm button commits to. */\n   public long totalCents()\n   {\n      return subtotalCents() + shippingCents();\n   }\n\n   /**\n    * Formats cents as money.\n    *\n    * @param cents the amount\n    * @return the formatted amount\n    */\n   public static String money(long cents)\n   {\n      return cents / 100 + \".\" + (cents % 100 < 10 ? \"0\" : \"\") + cents % 100;\n   }\n}\n"
    },
    {
      "path": "dev/cf/app/wizard/CustomerStep.java",
      "name": "CustomerStep.java",
      "code": "package dev.cf.app.wizard;\n\nimport dev.crossfly.ui.*;\nimport dev.crossfly.ui.graphics.*;\nimport dev.crossfly.ui.layout.*;\nimport dev.crossfly.ui.style.*;\n\nimport static dev.crossfly.ui.Widgets.*;\n\n/** Step 1 — who the order is for. Three fields, one of them optional, and that is the whole step. */\npublic class CustomerStep extends WizardStep\n{\n   private TextField customer, email, reference;\n\n   /**\n    * @param order the shared order\n    */\n   public CustomerStep(Order order)\n   {\n      super(order);\n   }\n\n   public String title()\n   {\n      return \"Customer\";\n   }\n\n   public int icon()\n   {\n      return 0xE7FD;   // person\n   }\n\n   public String tag()\n   {\n      return \"step.customer\";\n   }\n\n   public void build(Component body) throws Exception\n   {\n      body.add(LAY_AUTO, outgap(label(\"Who is this order for?\", 1.5f, true, Styles.textColor,\n         Position.START), 0, 0, 0, 1f));\n\n      customer = field(body, \"Customer\", order.customer, \"step.customer.name\");\n      email = field(body, \"E-mail\", order.email, \"step.customer.email\");\n      reference = field(body, \"Their reference (optional)\", order.reference,\n         \"step.customer.reference\");\n\n      // Saying which field is optional is worth one label. Without it every field looks required,\n      // and a user who has no PO number stops here and goes to find one.\n      body.add(LAY_AUTO, label(\"We will send the confirmation to this address.\", 1f, false,\n         Styles.captionColor, Position.START));\n   }\n\n   private TextField field(Component body, String caption, String value, String tag)\n      throws Exception\n   {\n      TextField f = new TextField(value == null ? \"\" : value);\n      f.caption(caption);\n      f.testTag(tag);\n      body.add(LAY_AUTO, outgap(f, 0, 0, 0, .8f));\n      return f;\n   }\n\n   public void save()\n   {\n      order.customer = customer.getText()\n         .trim();\n      order.email = email.getText()\n         .trim();\n      order.reference = reference.getText()\n         .trim();\n   }\n\n   public String problem()\n   {\n      if (order.customer.isEmpty())\n         return \"We need the customer's name.\";\n      // Deliberately not a regular expression. The only thing a client can usefully check is that\n      // the address is shaped like one; deciding whether it exists is the mail server's job, and an\n      // over-strict pattern rejects addresses that are perfectly valid.\n      if (order.email.indexOf('@') < 1 || order.email.indexOf('.', order.email.indexOf('@')) < 0)\n         return \"That e-mail address does not look complete.\";\n      return null;\n   }\n\n   public String[] summary()\n   {\n      return new String[] { \"Customer\", order.customer, \"E-mail\", order.email, \"Reference\",\n         order.reference.isEmpty() ? \"—\" : order.reference };\n   }\n}\n"
    },
    {
      "path": "dev/cf/app/wizard/ItemsStep.java",
      "name": "ItemsStep.java",
      "code": "package dev.cf.app.wizard;\n\nimport dev.crossfly.ui.*;\nimport dev.crossfly.ui.graphics.*;\nimport dev.crossfly.ui.layout.*;\nimport dev.crossfly.ui.style.*;\n\nimport static dev.crossfly.ui.Widgets.*;\n\n/**\n * Step 2 — what is being ordered.\n *\n * <p>The step with a list inside it, which is the one that makes a wizard interesting. Two things\n * follow from that and neither is obvious until it bites:</p>\n *\n * <ul>\n *   <li><b>It writes to the order as it goes</b>, not in {@code save()}. Add and remove are already\n *       edits to the order; keeping a shadow list and reconciling it later means two lists that can\n *       disagree, and the running total is computed from one of them.</li>\n *   <li><b>The running total is on screen the whole time.</b> A total the user only meets on the\n *       review page is a total that sends them back through the wizard.</li>\n * </ul>\n */\npublic class ItemsStep extends WizardStep\n{\n   private Component list;\n   private Label total;\n   private Dropdown product;\n   private TextField quantity;\n\n   /**\n    * @param order the shared order\n    */\n   public ItemsStep(Order order)\n   {\n      super(order);\n   }\n\n   public String title()\n   {\n      return \"Items\";\n   }\n\n   public int icon()\n   {\n      return 0xE8CC;   // shopping cart\n   }\n\n   public String tag()\n   {\n      return \"step.items\";\n   }\n\n   public void build(Component body) throws Exception\n   {\n      body.add(LAY_AUTO, outgap(label(\"What are they ordering?\", 1.5f, true, Styles.textColor,\n         Position.START), 0, 0, 0, 1f));\n\n      Component add = new Component();\n      add.setLayout(new GridLayout(Position.START, Position.CENTER));\n\n      product = new Dropdown(\"Product\", Order.PRODUCT);\n      product.testTag(\"step.items.product\");\n\n      quantity = new TextField(\"1\");\n      quantity.caption(\"Qty\");\n      quantity.keyboard(Keyboard.NUMERIC);\n      quantity.testTag(\"step.items.qty\");\n\n      Button plus = new Button(MaterialIcon.of(0xE145, 1.4f));   // add\n      plus.testTag(\"step.items.add\");\n      plus.onAction(c -> addLine());\n\n      add.add(1f, 0f, 0f, product, outgap(quantity, .5f, 0, .5f, 0), plus);\n      body.add(LAY_AUTO, outgap(add, 0, 0, 0, 1f));\n\n      list = new Component();\n      list.setLayout(new VerticalLayout(Position.START, Position.START));\n      list.testTag(\"step.items.list\");\n      body.add(LAY_AUTO, list);\n\n      total = label(\"\", 1.5f, true, Styles.textColor, Position.END);\n      total.testTag(\"step.items.total\");\n      body.add(LAY_AUTO, outgap(total, 0, .8f, 0, 0));\n\n      rebuildList();\n   }\n\n   private void addLine()\n   {\n      int p = product.getSelectedIndex();\n      if (p < 0)\n         return;\n      int q;\n      try\n      {\n         q = Integer.parseInt(quantity.getText()\n            .trim());\n      }\n      catch (NumberFormatException e)\n      {\n         q = 0;\n      }\n      if (q <= 0)\n         return;\n\n      // Same product twice is one line with a bigger quantity, not two lines. The alternative\n      // renders as a list that looks like a mistake and totals correctly, which is worse than\n      // either being right or being obviously wrong.\n      for (Order.Line l : order.lines)\n         if (l.product.equals(Order.PRODUCT[p]))\n         {\n            l.quantity += q;\n            rebuildList();\n            Screen.repaint();\n            return;\n         }\n      order.lines.add(new Order.Line(Order.PRODUCT[p], q, Order.PRICE[p]));\n      rebuildList();\n      Screen.repaint();\n   }\n\n   private void rebuildList()\n   {\n      try\n      {\n         list.removeAll();\n         // removeAll() removes the layout with the children — put it back first.\n         list.setLayout(new VerticalLayout(Position.START, Position.START));\n\n         if (order.lines.isEmpty())\n            list.add(LAY_AUTO, label(\"Nothing on this order yet.\", 1f, false, Styles.captionColor,\n               Position.START));\n\n         for (int i = 0; i < order.lines.size(); i++)\n         {\n            final Order.Line l = order.lines.get(i);\n            Card row = new Card();\n            row.setLayout(new GridLayout(Position.START, Position.CENTER));\n            row.getStyle()\n               .outgap(Gap.ofFont(0, 0, 0, .4f));\n\n            Component text = new Component();\n            text.setLayout(new VerticalLayout(Position.START, Position.START));\n            text.add(LAY_AUTO, label(l.product, 1.2f, true, Styles.textColor, Position.START));\n            text.add(LAY_AUTO, label(l.quantity + \" × \" + Order.money(l.unitCents), 1f, false,\n               Styles.captionColor, Position.START));\n\n            Button remove = new Button(MaterialIcon.of(0xE872, 1.2f));   // delete\n            remove.testTag(\"step.items.remove.\" + i);\n            remove.onAction(c ->\n            {\n               order.lines.remove(l);\n               rebuildList();\n               Screen.repaint();\n            });\n\n            row.add(1f, 0f, 0f, text, label(Order.money(l.totalCents()), 1.2f, true,\n               Styles.textColor, Position.END), outgap(remove, .6f, 0, 0, 0));\n            list.add(LAY_AUTO, row);\n         }\n\n         total.setText(\"Subtotal  \" + Order.money(order.subtotalCents()));\n         total.setValidLayout(false);\n         list.setValidLayout(false);\n      }\n      catch (Exception e)\n      {\n         dev.crossfly.util.Debug.handle(e);\n      }\n   }\n\n   public void save()\n   {\n      // Nothing to do: add and remove already wrote to the order. Leaving this empty is the point —\n      // there is no second copy that could be out of step.\n   }\n\n   public String problem()\n   {\n      return order.lines.isEmpty() ? \"Add at least one item.\" : null;\n   }\n\n   public String[] summary()\n   {\n      String[] out = new String[order.lines.size() * 2 + 2];\n      int k = 0;\n      for (Order.Line l : order.lines)\n      {\n         out[k++] = l.quantity + \" × \" + l.product;\n         out[k++] = Order.money(l.totalCents());\n      }\n      out[k++] = \"Subtotal\";\n      out[k] = Order.money(order.subtotalCents());\n      return out;\n   }\n}\n"
    },
    {
      "path": "dev/cf/app/wizard/DeliveryStep.java",
      "name": "DeliveryStep.java",
      "code": "package dev.cf.app.wizard;\n\nimport dev.crossfly.ui.*;\nimport dev.crossfly.ui.graphics.*;\nimport dev.crossfly.ui.layout.*;\nimport dev.crossfly.ui.style.*;\n\nimport static dev.crossfly.ui.Widgets.*;\n\n/**\n * Step 3 — how it gets there.\n *\n * <p>The step where one choice changes what the rest of the step asks: collection needs no address.\n * Hiding the address field rather than disabling it is the right call here — a disabled field\n * still reads as something you failed to fill in — and it is why {@link #problem()} checks the\n * address only when it is actually being asked for.</p>\n *\n * <p>The options are {@link Option}s in a group rather than a dropdown, because there are three of\n * them, they carry a price, and the user should be able to compare them without opening anything.\n * A dropdown is right when the list is long or the choice is uninteresting; here it is neither.</p>\n */\npublic class DeliveryStep extends WizardStep\n{\n   private Option[] options;\n   private TextField address;\n   private Component addressBox;\n   private Switch note;\n\n   /**\n    * @param order the shared order\n    */\n   public DeliveryStep(Order order)\n   {\n      super(order);\n   }\n\n   public String title()\n   {\n      return \"Delivery\";\n   }\n\n   public int icon()\n   {\n      return 0xE558;   // local shipping\n   }\n\n   public String tag()\n   {\n      return \"step.delivery\";\n   }\n\n   public void build(Component body) throws Exception\n   {\n      body.add(LAY_AUTO, outgap(label(\"How should it get there?\", 1.5f, true, Styles.textColor,\n         Position.START), 0, 0, 0, 1f));\n\n      Group group = new Group();\n      options = new Option[Order.SHIPPING.length];\n      for (int i = 0; i < options.length; i++)\n      {\n         final int idx = i;\n         String cost = Order.SHIPPING_COST[i] == 0 ? \"included\"\n            : Order.money(Order.SHIPPING_COST[i]);\n         Option o = new Option(Order.SHIPPING[i] + \"  ·  \" + cost);\n         o.testTag(\"step.delivery.option.\" + i);\n         o.setChecked(order.shipping == i);\n         o.onAction(c ->\n         {\n            order.shipping = idx;\n            showAddress(idx != 2);\n            Screen.repaint();\n         });\n         group.add(o);\n         options[i] = o;\n         body.add(LAY_AUTO, outgap(o, 0, 0, 0, .4f));\n      }\n\n      addressBox = new Component();\n      addressBox.setLayout(new VerticalLayout(Position.START, Position.START));\n      address = new TextField(order.address == null ? \"\" : order.address);\n      address.caption(\"Delivery address\");\n      address.testTag(\"step.delivery.address\");\n      addressBox.add(LAY_AUTO, outgap(address, 0, .8f, 0, .8f));\n      body.add(LAY_AUTO, addressBox);\n\n      note = new Switch(\"Include a delivery note\");\n      note.checked(order.deliveryNote);\n      note.testTag(\"step.delivery.note\");\n      body.add(LAY_AUTO, note);\n\n      showAddress(order.shipping != 2);\n   }\n\n   private void showAddress(boolean show)\n   {\n      addressBox.setVisible(show);\n      addressBox.setValidLayout(false);\n   }\n\n   public void save()\n   {\n      order.address = address.getText()\n         .trim();\n      order.deliveryNote = note.isChecked();\n      for (int i = 0; i < options.length; i++)\n         if (options[i].isChecked())\n            order.shipping = i;\n   }\n\n   public String problem()\n   {\n      if (order.shipping < 0)\n         return \"Choose how it should be delivered.\";\n      if (order.shipping != 2 && order.address.isEmpty())\n         return \"We need an address to deliver to.\";\n      return null;\n   }\n\n   public String[] summary()\n   {\n      return new String[] { \"Shipping\", order.shipping < 0 ? \"—\" : Order.SHIPPING[order.shipping],\n         \"Address\", order.shipping == 2 ? \"Collection\" : order.address,\n         \"Delivery note\", order.deliveryNote ? \"Yes\" : \"No\" };\n   }\n}\n"
    },
    {
      "path": "dev/cf/app/wizard/ReviewStep.java",
      "name": "ReviewStep.java",
      "code": "package dev.cf.app.wizard;\n\nimport java.util.List;\n\nimport dev.crossfly.ui.*;\nimport dev.crossfly.ui.graphics.*;\nimport dev.crossfly.ui.layout.*;\nimport dev.crossfly.ui.style.*;\n\nimport static dev.crossfly.ui.Widgets.*;\n\n/**\n * Step 4 — the review, which is the step a wizard exists for.\n *\n * <p>Every step before this one asked for something. This one is the only place the user sees the\n * whole thing at once, and it is where they either commit or notice the mistake. Three properties\n * make it work, and all three are about not duplicating the steps:</p>\n *\n * <ul>\n *   <li><b>It is built from {@link WizardStep#summary()}</b>, not from the order directly. A review\n *       that formats the order itself is a second implementation of every step's presentation, and\n *       it goes stale the first time a step changes what it collects.</li>\n *   <li><b>Every block links back to the step that filled it.</b> The user who spots a wrong e-mail\n *       here wants to fix the e-mail, not to press Back three times counting screens.</li>\n *   <li><b>The total is stated once, at the bottom, in full.</b> This is the number being agreed to;\n *       burying it in a row of grey labels is how disputes start.</li>\n * </ul>\n */\npublic class ReviewStep extends WizardStep\n{\n   private final List<WizardStep> steps;\n   private final WizardApp app;\n\n   /**\n    * @param order the shared order\n    * @param steps the steps to summarise — this one is not among them\n    * @param app   the host, so an \"Edit\" link can jump back to a step\n    */\n   public ReviewStep(Order order, List<WizardStep> steps, WizardApp app)\n   {\n      super(order);\n      this.steps = steps;\n      this.app = app;\n   }\n\n   public String title()\n   {\n      return \"Review\";\n   }\n\n   public int icon()\n   {\n      return 0xE8F4;   // fact check\n   }\n\n   public String tag()\n   {\n      return \"step.review\";\n   }\n\n   public void build(Component body) throws Exception\n   {\n      body.add(LAY_AUTO, outgap(label(\"Check this over\", 1.5f, true, Styles.textColor,\n         Position.START), 0, 0, 0, .2f));\n      body.add(LAY_AUTO, outgap(label(\"Nothing is sent until you confirm.\", 1f, false,\n         Styles.captionColor, Position.START), 0, 0, 0, 1f));\n\n      for (int i = 0; i < steps.size(); i++)\n         body.add(LAY_AUTO, outgap(block(steps.get(i), i), 0, 0, 0, .6f));\n\n      Card totals = new Card();\n      totals.setLayout(new VerticalLayout(Position.START, Position.START));\n      totals.testTag(\"step.review.totals\");\n      totals.add(LAY_AUTO, line(\"Subtotal\", Order.money(order.subtotalCents()), false));\n      totals.add(LAY_AUTO, line(\"Shipping\", order.shippingCents() == 0 ? \"included\"\n         : Order.money(order.shippingCents()), false));\n      totals.add(LAY_AUTO, line(\"Total\", Order.money(order.totalCents()), true));\n      body.add(LAY_AUTO, outgap(totals, 0, .6f, 0, 0));\n   }\n\n   private Component block(final WizardStep s, final int index) throws Exception\n   {\n      Card c = new Card();\n      c.setLayout(new VerticalLayout(Position.START, Position.START));\n      c.testTag(\"step.review.block.\" + index);\n\n      Component head = new Component();\n      head.setLayout(new GridLayout(Position.START, Position.CENTER));\n      Button edit = new Button(\"Edit\");\n      edit.testTag(\"step.review.edit.\" + index);\n      edit.onAction(e -> app.goToStep(index));\n      head.add(1f, 0f, label(s.title(), 1.2f, true, Styles.textColor, Position.START), edit);\n      c.add(LAY_AUTO, outgap(head, 0, 0, 0, .5f));\n\n      String[] pairs = s.summary();\n      for (int i = 0; i + 1 < pairs.length; i += 2)\n         c.add(LAY_AUTO, line(pairs[i], pairs[i + 1], false));\n      return c;\n   }\n\n   private Component line(String name, String value, boolean strong) throws Exception\n   {\n      Component c = new Component();\n      c.setLayout(new GridLayout(Position.START, Position.CENTER));\n      c.getStyle()\n         .outgap(Gap.ofFont(0, 0, 0, .3f));\n      c.add(.5f, .5f, label(name, strong ? 1.2f : 1f, strong, strong ? Styles.textColor\n         : Styles.captionColor, Position.START), label(value == null || value.isEmpty() ? \"—\"\n            : value, strong ? 1.5f : 1f, true, Styles.textColor, Position.END));\n      return c;\n   }\n\n   public void save()\n   {\n      // Nothing to save: this step collects nothing. It reads.\n   }\n\n   public String problem()\n   {\n      // The steps before this one already answered for themselves; asking again here would only\n      // duplicate their rules and let the two drift apart.\n      return null;\n   }\n\n   public String[] summary()\n   {\n      return new String[0];\n   }\n}\n"
    }
  ],
  "lines": 1017,
  "sharedFiles": [
    {
      "path": "dev/cf/app/AppBar.java",
      "name": "AppBar.java",
      "code": "package dev.cf.app;\n\nimport dev.crossfly.ui.*;\nimport dev.crossfly.ui.graphics.*;\nimport dev.crossfly.ui.layout.*;\nimport dev.crossfly.ui.style.*;\n\nimport static dev.crossfly.ui.Widgets.*;\n\n/**\n * The top bar the four example apps share: back, title, and up to a couple of actions.\n *\n * <p>There is no top bar in CrossFly, and that is not an omission. A title bar is the single most\n * app-specific piece of chrome there is — height, colour, whether it collapses, whether the title\n * centres, what sits on the right — and a framework that decided those would be fought by every\n * app that used it. What the framework supplies is what a bar is <i>made of</i>: a container, a\n * layout, buttons, glyphs, and a style that follows the skin.</p>\n *\n * <p>So this is an <b>app-level</b> class, shared by the four examples here because they are four\n * examples in one repository — in a real project it would live in your own {@code ui} package. It\n * is about forty lines, which is roughly what a top bar costs, and it is worth reading as the\n * answer to \"how do I compose chrome out of primitives\".</p>\n *\n * <pre>\n * add(0f, new AppBar(\"Part\", this::hide).action(0xE3C9, this::edit));\n * </pre>\n */\npublic class AppBar extends Component\n{\n   private final String title;\n   private final Runnable onBack;\n\n   private final int[] icons = new int[3];\n   private final Runnable[] actions = new Runnable[3];\n   private final String[] tags = new String[3];\n   private int count;\n\n   /**\n    * @param title  the heading\n    * @param onBack what the back arrow does, or null for a bar with no back arrow\n    */\n   public AppBar(String title, Runnable onBack)\n   {\n      this.title = title;\n      this.onBack = onBack;\n   }\n\n   /**\n    * Adds a right-hand action. Up to three; past that the answer is a menu, not a fourth glyph.\n    *\n    * @param codepoint the Material Icons codepoint\n    * @param tag       the test tag for the button\n    * @param action    what it does\n    * @return this bar, for chaining\n    */\n   public AppBar action(int codepoint, String tag, Runnable action)\n   {\n      if (count < icons.length)\n      {\n         icons[count] = codepoint;\n         tags[count] = tag;\n         actions[count] = action;\n         count++;\n      }\n      return this;\n   }\n\n   @Override\n   public void onAdd(boolean firstTime) throws Exception\n   {\n      if (!firstTime)\n         return;\n      // A strip of surface with a hairline of elevation under it. Both colours are theme tokens, so\n      // the bar is right in dark mode and under every skin without a branch anywhere — and the\n      // shadow is expressed as a ratio of the font height, so it survives a high-DPI screen, where\n      // a shadow measured in pixels all but disappears.\n      getStyle().backColor(Styles.backColor)\n         .shadow(0, .06f, .25f, Styles.borderColor)\n         .ingap(Gap.ofFont(.5f, .4f, .5f, .4f));\n\n      setLayout(new GridLayout(Position.START, Position.CENTER));\n\n      // A bar with no back arrow gets no COLUMN for one. The obvious shortcut — always build the\n      // column and fill it with a bare `new Component()` — silently kills the entire row: an empty\n      // container has no layout, an auto-width column cannot measure it, and the bar comes out\n      // zero-high with the title inside it. It looks like the title was never added.\n      boolean hasBack = onBack != null;\n      int columns = (hasBack ? 1 : 0) + 1 + count;\n      Object[] cells = new Object[columns * 2];\n\n      int k = 0;\n      if (hasBack)\n         cells[k++] = 0f;                  // back: its own width\n      cells[k++] = 1f;                     // title: everything left over\n      for (int i = 0; i < count; i++)\n         cells[k++] = 0f;                  // actions: their own width\n\n      if (hasBack)\n      {\n         Button b = new Button(MaterialIcon.of(0xE5C4, 1.4f));   // arrow_back\n         b.testTag(\"bar.back\");\n         b.onAction(c -> onBack.run());\n         b.getStyle()\n            .outgap(Gap.ofFont(0, 0, .5f, 0));\n         cells[k++] = b;\n      }\n\n      Label t = label(title, 1.5f, true, Styles.textColor, Position.START);\n      t.testTag(\"bar.title\");\n      cells[k++] = t;\n\n      for (int i = 0; i < count; i++)\n      {\n         Button b = new Button(MaterialIcon.of(icons[i], 1.4f));\n         final Runnable a = actions[i];\n         b.testTag(tags[i]);\n         b.onAction(c -> a.run());\n         b.getStyle()\n            .outgap(Gap.ofFont(.4f, 0, 0, 0));\n         cells[k++] = b;\n      }\n\n      add(cells);\n   }\n}\n"
    },
    {
      "path": "dev/cf/app/AppTheme.java",
      "name": "AppTheme.java",
      "code": "package dev.cf.app;\n\nimport dev.crossfly.ui.color.ColorScheme;\nimport dev.crossfly.ui.style.Styles;\n\n/**\n * The one line that gives an app its colour, and the reason the four examples here have one.\n *\n * <p><b>An app declares its brand; it does not pick colours.</b> One seed goes in, and CrossFly\n * derives the whole scheme from it — primary, surface, on-surface, containers, the tonal fills\n * behind an active tab, the ring of a gauge, both themes. Nothing downstream names a colour, which\n * is what makes dark mode and the skin picker work without a single branch.</p>\n *\n * <p>Skip this and the app is not \"unstyled\", it is <b>grey</b>: the skin's neutral palette is what\n * you get when nobody declared anything, and it looks exactly like a bug. That is what happened to\n * these four — they were correct from the samples menu, which declares a brand of its own, and\n * monochrome when launched straight from {@code classname}, which is how anybody copying them would\n * run them.</p>\n *\n * <h3>Why enter/leave and not just a call at startup</h3>\n *\n * <p>The brand is global state. A real app declares it once in {@code Main} and never thinks about\n * it again — and if that is your case, {@code Styles.setBrand(BRAND)} at startup is the whole story\n * and this class is more machinery than you need.</p>\n *\n * <p>These four are different: they are guests. They open from a menu that has its own brand, and\n * from a test suite whose later screenshots are compared against committed images. A guest that\n * changes global state and walks away leaves its host repainted in someone else's colour — the\n * samples menu comes back blue, the goldens fail three tests later, and neither failure points at\n * the app that caused it. So each app calls {@link #enter()} when it opens and {@link #leave()}\n * when it closes, and the palette that was in force is handed back.</p>\n */\npublic final class AppTheme\n{\n   /**\n    * The seed every colour in the four example apps is derived from.\n    *\n    * <p>Deep blue, chosen against orange for one reason: orange is the brand of the production app\n    * these examples were extracted from, and an example that wears it invites the reader to think\n    * the two are related. For orange instead, this is the only line to change —\n    * {@code 0xFFE2622C} is a good one. Nothing else in the four apps names a colour.</p>\n    */\n   public static final int BRAND = 0xFF1565C0;\n\n   /**\n    * The page behind the cards: the surface, pulled a little way towards the brand.\n    *\n    * <p><b>Why a page needs its own colour at all.</b> Cards, buttons and fields are all drawn on\n    * the skin's surface, which in a light theme is white or near it. Put them on a white page and\n    * every edge disappears — the card stops reading as a card and a white button on white looks\n    * like a mistake rather than a control. What separates them is not a border, it is the page\n    * being a shade the foreground is not.</p>\n    *\n    * <p><b>Why it is derived and not a hex.</b> {@code 0xFFF2F5F9} would be a perfectly nice light\n    * blue and would be wrong twice: it stops being light blue the moment somebody changes\n    * {@link #BRAND} to orange, and it stays light in dark mode, where the page must go darker than\n    * the cards rather than lighter. Mixing the primary into the surface at 8% follows both — the\n    * tint always matches the brand, and in a dark theme the same call yields a deep, slightly\n    * tinted ground.</p>\n    *\n    * <p>Read it through {@code onRestyle} rather than once, so a theme flip re-derives it.</p>\n    *\n    * @return the ARGB page colour for the palette currently in force\n    */\n   public static int pageColor()\n   {\n      return dev.cf.Tones.page();\n   }\n\n   /** What was in force before the outermost {@link #enter()}. */\n   private static Integer previousBrand;\n   private static ColorScheme previousScheme;\n\n   /** How many example apps are open. Restores only when the last one closes. */\n   private static int depth;\n\n   private AppTheme()\n   {\n   }\n\n   /**\n    * Declares {@link #BRAND}, remembering whatever was in force.\n    *\n    * <p>Call it first thing in {@code onAdd}. It triggers a restyle, so every window already open\n    * re-resolves its colours — including this one, which is why it can run before the screen is\n    * built.</p>\n    */\n   public static void enter()\n   {\n      if (depth++ == 0)\n      {\n         previousBrand = Styles.hasBrand() ? Integer.valueOf(Styles.brand()) : null;\n         ColorScheme live = Styles.colorScheme();\n         if (live != null)\n         {\n            // A SNAPSHOT, never the reference. Styles keeps ONE ColorScheme instance for the life\n            // of the app and mutates its roles in place on every seed change — deliberately, so\n            // that a PalColor captured into a style at build time keeps resolving and follows the\n            // theme forever. The consequence for anybody saving and restoring: the object you kept\n            // IS the object about to be overwritten, so handing it back restores nothing at all.\n            // This cost four broken golden images in a suite that had been green all morning.\n            previousScheme = ColorScheme.fromSeed(BRAND, live.isDark);\n            previousScheme.copyFrom(live);\n         }\n      }\n      Styles.setBrand(BRAND);\n   }\n\n   /**\n    * Hands the palette back to the host.\n    *\n    * <p>Call it from {@code onRemove}, which CrossFly runs on {@code hide()}. Two cases: the host\n    * had declared a brand, so re-declaring it regenerates everything; or nothing was declared, so\n    * the declaration is dropped <b>and</b> the scheme that was actually in force is restored —\n    * clearing the brand alone would leave this app's blue behind, still painted, just no longer\n    * owned by anybody.</p>\n    */\n   public static void leave()\n   {\n      if (depth > 0 && --depth == 0)\n      {\n         if (previousBrand != null)\n            Styles.setBrand(previousBrand.intValue());\n         else\n         {\n            Styles.clearBrand();\n            if (previousScheme != null)\n               Styles.setColorScheme(previousScheme);\n         }\n         previousBrand = null;\n         previousScheme = null;\n      }\n   }\n}\n"
    }
  ],
  "page": "https://crossfly.dev/examples/app-wizard",
  "run": "https://crossfly.dev/playground/index.html?ex=app-wizard",
  "runLocally": "gradlew lwjgl3:run --args=\"classname dev.cf.app.wizard.WizardApp\"",
  "license": "See crossfly.dev for licensing. The code is published to be copied into your own project."
}