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

Telling the user something

Interrupt in proportion to what you need back. A confirmation on something reversible teaches people to dismiss confirmations — including the one that mattered.

What this shows

Four ways to tell the user something, and the question that picks between them.

The question is what do you need from them: nothing, a decision, a choice from a few, or nothing but they must not miss it. Getting this wrong is why apps end up asking "OK?" about things that were never in doubt — and why the one destructive action arrives as a toast nobody reads.

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.R11Telling"
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.

ActionSheetAlertButtonCardComponentHintPalColorStylesWidgets

The source

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

Raw
dev/cf/recipe/R11Telling.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.*;

/**
 * Four ways to tell the user something, and the question that picks between them.
 *
 * <p>The question is <b>what do you need from them</b>: nothing, a decision, a choice from a few,
 * or nothing but they must not miss it. Getting this wrong is why apps end up asking "OK?" about
 * things that were never in doubt — and why the one destructive action arrives as a toast nobody
 * reads.</p>
 */
public class R11Telling extends Recipe
{
   private static final int IC_HINT = 0xE7F4, IC_ALERT = 0xE002, IC_SHEET = 0xE5D2,
      IC_INLINE = 0xE000;

   public String title()
   {
      return "Telling the user something";
   }

   public String note()
   {
      return "Interrupt in proportion to what you need back. A confirmation on something reversible"
         + " teaches people to dismiss confirmations — including the one that mattered.";
   }

   public void build(Component body) throws Exception
   {
      // The tones climb with the interruption: the one that needs nothing back wears the quiet
      // supporting tone, the one that stops the user and the one they must fix wear the error
      // tone. Four identical grey cards said the opposite of what the screen is about.
      row(body, IC_HINT, Tones.support(), "Hint — nothing needed", "It happened. A toast that"
         + " fades, over the screen the user is already looking at.",
         () -> Hint.show("Saved", 2000));

      row(body, IC_SHEET, Tones.highlight(), "ActionSheet — one of a few", "Three or four actions"
         + " on one object. Cheaper than a screen, clearer than a long-press nobody discovers.",
         this::sheet);

      row(body, IC_ALERT, Tones.danger(), "Alert — a decision", "Two ways forward and the app"
         + " cannot pick. Name the thing in the question: \"Delete this?\" is not answerable.",
         this::alert);

      row(body, IC_INLINE, Tones.danger(), "Inline — they must fix it", "A validation message"
         + " belongs next to the field, not in a dialog they have to dismiss before they can act"
         + " on it.", null);
   }

   private void alert()
   {
      Alert a = new Alert("Discard this draft?", "It has not been sent, and this cannot be undone.",
         "Keep editing", "Discard");
      // Index 1 is the second button. The destructive one goes LAST: the safe choice is where the
      // thumb rests, and the dangerous one costs a deliberate reach.
      a.buttonAction(1, c -> Hint.show("Discarded", 1500));
      a.show();
   }

   private void sheet()
   {
      ActionSheet s = new ActionSheet("Cancel", "Share", "Duplicate", "Export");
      for (int i = 0; i < 3; i++)
      {
         final int idx = i;
         s.buttonAction(idx, c -> Hint.show(new String[] { "Shared", "Duplicated", "Exported" }[idx],
            1500));
      }
      s.show();
   }

   private void row(Component body, int icone, PalColor tom, String title, String why,
      Runnable demo) throws Exception
   {
      Card c = Tones.card(tom);
      c.add(LAY_AUTO, outgap(Tones.head(icone, title, tom), 0, 0, 0, 0));
      c.add(LAY_AUTO, outgap(label(why, 1f, false, Styles.captionColor, Position.START), 0, .2f, 0,
         demo == null ? 0 : .5f));
      if (demo != null)
      {
         Button b = Tones.action("Show it", tom);
         b.onAction(e -> demo.run());
         c.add(LAY_AUTO, b);
      }
      body.add(LAY_AUTO, outgap(c, 0, 0, 0, .6f));
   }
}
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
PreviousA form that fights backNextBottom nav that becomes a rail