Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started
All examples
RecipesFeedbackLists160 lines · 1 files

Empty, loading and broken

Three of these are usually left to render as blank space. Blank space reads as a bug, never as an answer.

What this shows

The four states every screen has, and the three nobody designs.

A screen is drawn once, for the case where the data arrived and there is plenty of it. Then it ships, and users meet the other three: nothing yet, nothing matching, and something went wrong. All three render as an empty rectangle unless somebody decided otherwise, and an empty rectangle says only one thing to the person looking at it — this is broken.

The distinction that matters most is the one between the first two. "You have not added anything yet" invites an action and is a good first impression. "Nothing matches that search" is an answer to a question the user asked. Showing the first when the second is true tells a long-standing customer their account is empty.

Run it on your machine

Every example is a plain Window, so the launcher only needs its name.

gradlew lwjgl3:run --args="classname dev.cf.recipe.R30Empty"
Editing it

Java is compiled ahead of time, so the browser cannot rebuild an edit on its own — the code you see here was turned into JavaScript by TeaVM before it was published. Copy the file into a project and it runs unchanged on all four platforms; the command above is the whole setup.

Running hereCrossFly 0.1.32
Starting the app…

Compiled to WebAssembly-era JavaScript by TeaVM — the same code, not a video of it. · All examples

What it uses

Framework types this example touches. The linked ones have a reference page.

ButtonCardChipComponentDebugGapGridLayoutHintHorizontalLayoutPalColorScreenStylesVerticalLayoutWidgets

The source

Complete and unabridged. This is the file, not an excerpt of it.

Raw
dev/cf/recipe/R30Empty.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.ui.*;
import dev.crossfly.ui.color.*;
import dev.crossfly.ui.graphics.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * The four states every screen has, and the three nobody designs.
 *
 * <p>A screen is drawn once, for the case where the data arrived and there is plenty of it. Then it
 * ships, and users meet the other three: nothing yet, nothing matching, and something went wrong.
 * All three render as an empty rectangle unless somebody decided otherwise, and an empty rectangle
 * says only one thing to the person looking at it — <b>this is broken</b>.</p>
 *
 * <p>The distinction that matters most is the one between the first two. "You have not added
 * anything yet" invites an action and is a good first impression. "Nothing matches that search" is
 * an answer to a question the user asked. Showing the first when the second is true tells a
 * long-standing customer their account is empty.</p>
 */
public class R30Empty extends Recipe
{
   private enum State
   {
      LOADED, FIRST_RUN, NO_MATCH, FAILED
   }

   private State state = State.FIRST_RUN;
   private Component stage;

   public String title()
   {
      return "Empty, loading and broken";
   }

   public String note()
   {
      return "Three of these are usually left to render as blank space. Blank space reads as a bug,"
         + " never as an answer.";
   }

   public void build(Component body) throws Exception
   {
      Component switcher = new Component();
      switcher.setLayout(new HorizontalLayout(Position.START, Position.CENTER));
      for (final State s : State.values())
      {
         Chip c = new Chip(s.name()
            .toLowerCase()
            .replace('_', ' '));
         c.setSelected(s == state);
         c.getStyle()
            .outgap(Gap.ofFont(0, 0, .4f, 0));
         c.onAction(e ->
         {
            state = s;
            rebuild();
            Screen.repaint();
         });
         switcher.add(LAY_AUTO, c);
      }
      body.add(LAY_AUTO, outgap(switcher, 0, 0, 0, 1f));

      stage = new Component();
      stage.setLayout(new VerticalLayout(Position.START, Position.START));
      stage.testTag("recipe.states");
      body.add(LAY_AUTO, stage);
      rebuild();
   }

   private void rebuild()
   {
      try
      {
         stage.removeAll();
         // removeAll() takes the layout with it.
         stage.setLayout(new VerticalLayout(Position.START, Position.START));

         switch (state)
         {
            case LOADED:
               for (int i = 1; i <= 4; i++)
               {
                  Card c = new Card();
                  c.setLayout(new GridLayout(Position.START, Position.CENTER));
                  c.getStyle()
                     .outgap(Gap.ofFont(0, 0, 0, .5f));
                  Component t = new Component();
                  t.setLayout(new VerticalLayout(Position.START, Position.START));
                  t.add(LAY_AUTO, label("Order " + (4180 + i), 1.2f, true, Styles.textColor,
                     Position.START));
                  t.add(LAY_AUTO, label("Placed this week", 1f, false, Styles.captionColor,
                     Position.START));
                  c.add(0f, 1f, Tones.tile(0xE8CC, Tones.cycle(i)), outgap(t, .6f, 0, 0, 0));
                  stage.add(LAY_AUTO, c);
               }
               break;

            case FIRST_RUN:
               // An invitation, with the action right there. This is a first impression, and the
               // one empty state worth spending a sentence on.
               // An invitation is not a problem: it gets the tone of the thing being offered.
               stage.add(LAY_AUTO, state(0xE8CC, Tones.accent(), "No orders yet",
                  "When you place one, it will show up here.", "Place your first order"));
               break;

            case NO_MATCH:
               // An ANSWER to what the user typed, not an invitation. Offering "add your first
               // order" to somebody who searched for one they know exists reads as being ignored.
               // Nothing is broken — the search simply has no answer. Not red.
               stage.add(LAY_AUTO, state(0xE8B6, Tones.support(), "Nothing matches “ironvale”",
                  "Check the spelling, or clear the search to see everything.",
                  "Clear the search"));
               break;

            case FAILED:
               // What happened, and what they can do. Not the exception text: it names something
               // the user has no way to act on, and it frightens them.
               // The only one of the three that actually failed, and the only one in the error
               // tone. Three identical grey glyphs said all three were the same event.
               stage.add(LAY_AUTO, state(0xE000, Tones.danger(), "We could not load your orders",
                  "Nothing was lost. Check your connection and try again.", "Try again"));
               break;

            default:
               break;
         }
         stage.setValidLayout(false);
      }
      catch (Exception e)
      {
         dev.crossfly.util.Debug.handle(e);
      }
   }

   /** One shape for all three: a glyph, a sentence, a reason, and exactly one way forward. */
   private Component state(int glyph, PalColor tom, String title, String detail, String action)
      throws Exception
   {
      Component c = new Component();
      c.setLayout(new VerticalLayout(Position.CENTER, Position.CENTER));
      c.getStyle()
         .ingap(Gap.ofFont(1f, 2f, 1f, 2f));
      c.add(LAY_AUTO, outgap(matglyph(glyph, tom, 3f), 0, 0, 0, .8f));
      c.add(LAY_AUTO, outgap(label(title, 1.5f, true, Styles.textColor, Position.CENTER), 0, 0, 0,
         .3f));
      c.add(LAY_AUTO, outgap(label(detail, 1f, false, Styles.captionColor, Position.CENTER), 0, 0,
         0, 1f));
      Button b = Tones.action(action, tom);
      b.onAction(e -> Hint.show(action, 1500));
      c.add(LAY_AUTO, b);
      return c;
   }
}
For an AI assistant

Machine-readable versions of this page, so a model can read the shelf instead of guessing at the API.

This example, as JSONThis example, as Markdown
PreviousBrand and dark modeNextFiles, sharing and printing