Practice

Design rules for tools

A shared look and a shared set of manners, so that twenty tools written by ten people still feel like one product.

Every pyNavis tool is written by a different person, at a different time, for a different job. Nothing enforces consistency between them. What the user sees is not your tool, it is the pile: a ribbon tab of buttons and dock panels that all claim to belong together, and the first one that looks or behaves differently makes the whole tab feel improvised. The rules here apply to every surface you draw, a dialog, a pane.xaml panel, or a window of your own.

This page is the contract. The first half is what a tool has to look like, taken from the pyNavis design system v2.2. The second half is how it has to behave, which matters more and is easier to get wrong. Both halves are short on purpose.

The direction, in one paragraph#

Neutral grays, hairlines and whitespace carry the structure. The Windows accent appears in exactly three places: the primary action, the selection rail, and the focus ring. Numbers are quiet tabular text, not chips. Controls are the native affordances users already know, meaning checkbox, toggle, segmented and standard captions. Each tool gets one signature moment, the 28px weight-300 readout number pair. Solid surfaces only.

If you take one thing from that: your tool should be boring to look at and obvious to use. The interesting part is the model, not the chrome.

Accent in three places#

A tool window Rename viewpoints Level 04 Level 04 north elevation 412 Level 04 plant room 96 Level 04 riser 8 Cancel Rename 1. Focus ring 2px accent, offset 1, on every focusable thing 2. Selection rail 3px accent on a neutral row fill, never a tint 3. Primary action one per window; the foreground is picked by luminance, not always white
Three uses of the accent, and no fourth. The counts on the right are muted tabular text, the rows are 32px, everything else is neutral.
The accent is not a fixed colour

It is whatever the user chose in Windows, read at runtime. It can be pale yellow. White text on it is therefore not automatic: pick the foreground by luminance. Any design that assumes a particular accent hue is already broken on somebody's machine.

The slop test#

Before shipping any surface, check it against the tells below. If a screenshot of your tool could pass for a template demo, it fails. The goal is an interface a Windows user trusts instantly and then stops noticing.

Banned tellDo this instead
Gradients in text, fills or backgrounds. Anywhere. Solid surfaces. Paper, Surface, and hairlines between them.
Glassmorphism: blur, translucency, acrylic on content. An opaque surface fill. Content must never show through content.
Tint-soup: accent-alpha washes as default backgrounds or borders. Neutral gray backgrounds; the accent only in its three places.
Pill-soup: rounded chips carrying plain data. Text. A count is right-aligned muted tabular text at the row's edge.
Badge marketing: "Recommended" stickers, sparkles, emoji in the UI. Make the recommendation the default, and write a caption saying why.
Hero cards for what is simply a choice between two options. Two radio buttons, or a segmented control, with captions.
Centered content in rows, lists or forms. One left edge that everything aligns to. Numbers align right.
Hover-only affordances: controls invisible until pointed at. Visible at rest: circled expander chevrons, a real resize grip, present scrollbars.
Decorative motion: entrance animations, fades for their own sake. Motion only as state feedback, 120ms, ease-out, or none at all.
Opacity as disabled: dimming a control that still responds. A real disabled state: surface fill, muted text and border, and a caption saying why.
Mouse-only controls: anything Tab cannot reach, or a field that traps Tab. Every control in the tab order, with a visible focus ring.
A control per row: one element built for every item in a list. Virtualize. A list of thousands that builds thousands of controls is a defect.
Blank waiting: an empty framed box where a state belongs. Declare the empty state, the no-results state and the busy state before you ship.
Hand-drawn OS chrome: a custom titlebar imitating the real one. The real Windows titlebar. Always.
The one exception in the system

Shadows are banned except on a popup floating over its own window, and semantic colour is kept off borders except on the toast, which floats free of any chrome and has nothing around it to read its level against. Two exceptions, both written down. Do not invent a third.

Type#

RuleValue
UI typefaceSegoe UI Variable, falling back to Segoe UI
MonospaceConsolas, for script output, paths and code
Body size13px, the default for everything unless stated
Smallest permitted size11.5px. Nothing smaller ships.
The signature readout28px, weight 300, a number and its label as a pair. One per tool.
CaseSentence case, in labels, buttons, headings and messages alike
All capsNever, including for section headers
Em dashes and en dashesNever. Use a comma, a colon, a semicolon, or two sentences.
NumbersTabular figures, right-aligned, muted

Layout#

MeasureValue
Page padding, prose and settings windows24px
Page padding, data-dense windows16px
Gap between sections24px
Gap between adjacent controls8px
List row height32px
Control height32px
Corner radius4px, everywhere, with no exceptions
ShadowsNone, except a popup floating over its own window

Colour#

Twelve tokens plus the accent. If a colour you want is not in this table, the answer is one of these tokens, not a new colour.

TokenLightDarkUsed for
Paper#FFFFFF#202020Window and page background
Surface#F7F7F7#2B2B2BTable headers, code blocks, footers
Line#E0E0E0#3D3D3DRow separators, card edges
LineStrong#C9C9C9#4D4D4DControl borders
Frame#A6A6A6#6E6E6EGroup frames, the bold structural line
Ink#1A1A1A#E8E8E8Body text
Muted#6B6B6B#9A9A9ACaptions, counts, secondary columns
Selection#F0F0F0#333333Selected row fill. Neutral, never an accent tint.
Error#C50F1F#FF7B72Error text and rails
Success#0F7B0F#4CC44CSuccess toasts
Info#0078D4#4CC2FFInformational toasts
Warning#9D5D00#FFC83DWarning toasts and partial results
AccentThe user's Windows accent colour, read at runtime Primary action, selection rail, focus ring

Read the current theme with pynavis.script.is_dark_theme() and paint your own dialogs from the correct column. Read it when you build the window, not once at import time.

python
from pynavis import script

dark = script.is_dark_theme()
paper = '#202020' if dark else '#FFFFFF'
ink   = '#E8E8E8' if dark else '#1A1A1A'
muted = '#9A9A9A' if dark else '#6B6B6B'
Selection is neutral

The selected row's background is Selection, a plain gray. The accent appears only as the 3px rail on its left edge. Filling the row with an accent tint is tint-soup, it fights the accent on the primary button, and it collapses to unreadable on a pale Windows accent.

How a tool has to behave#

The visual rules keep tools looking alike. These keep them trustworthy, which is the harder half. Each one has a failure mode a real tool has shipped with.

Report every outcome#

A tool that does its job and shows nothing is indistinguishable from a tool that crashed silently. Every path out of your script ends in something the user can see, with the single exception below.

Do
count = purge_unused(doc)
if count:
    toast.success('Purged %d unused items' % count)
else:
    toast.info('Nothing to purge')
Don't
count = purge_unused(doc)
if count:
    toast.success('Purged %d unused items' % count)
# zero purged: the tool looks broken

Cancelling is silent#

The exception. When the user cancels, say nothing at all. They know what they did, and "Cancelled" is a notification about their own hand.

Do
if forms.confirm('Delete 412 viewpoints?'):
    delete()
    toast.success('Deleted 412 viewpoints')
# No is not an outcome to report
Don't
if forms.confirm('Delete 412 viewpoints?'):
    delete()
    toast.success('Deleted 412 viewpoints')
else:
    toast.info('Cancelled')

Failure is a sentence, not a traceback#

Predictable failures, meaning a missing file, a locked document, a value the user typed wrong, get caught and reported in plain words that say the fix. Genuine bugs should still raise: the runtime puts the traceback in the output window, which is exactly where you want it.

Do
try:
    data = json.load(open(path))
except (IOError, ValueError):
    toast.error('Could not read the mapping file',
                path)
else:
    apply_mapping(data)
Don't
try:
    data = json.load(open(path))
except Exception as exc:
    forms.alert(str(exc))     # modal, and
                              # says nothing useful
    raise

One undo step per user action#

Ten thousand separate edits give the user ten thousand Ctrl+Z presses, and they will also exhaust the host's undo stack, which quietly makes the work irreversible. Wrap a bulk edit in a single transaction.

Do
from pynavis import viewpoints

with viewpoints.transaction('Rename viewpoints'):
    for view in views:
        view.DisplayName = new_name(view)
Don't
for view in views:
    view.DisplayName = new_name(view)
# 4,200 undo steps, and the stack
# drops the earliest of them

Counted progress, and a way out#

Show progress for anything that can run longer than a second or two, and offer Cancel for anything that can run longer than several seconds. Put the count in the label: "Renaming 4,200 of 11,038" tells the user whether to wait or to stop, and a percentage alone does not.

Do
total = len(items)
for i, item in enumerate(items):
    process(item)
    if i % 100 == 0:
        output.progress(float(i) / total,
                        'Checking %d of %d' % (i, total))
output.progress(1.0)
Don't
for item in items:
    process(item)
    output.progress(0.5, 'Working...')
# indeterminate, and repainted
# once per item

When a run is cancelled part way, report the split honestly. "Deleted 3,180 of 4,200. 1,020 were left when you cancelled." That is a warning, not a success, and it must never claim work was undone when it was not.

Empty states are states#

Nothing selected, no results, no active document: each of these is a real answer and needs saying, along with what the user should do about it.

Do
items = selection.get_items()
if not items:
    toast.info('Nothing selected',
               'Select the items to export, '
               'then run this again.')
Don't
items = selection.get_items()
export(items)          # writes an empty
                       # file and says
                       # "Exported 0 items"

Never open a window to say one line#

A bare print() opens the whole output window to show one sentence, and if that is all the script writes, the user gets a nearly empty window. It reads as a bug because it looks exactly like one.

Do
toast.success('Saved 3 viewpoints')
Don't
print('Saved 3 viewpoints')

Speak the model's language#

The user knows what they see in Navisworks. They do not know your variable names, and they should never meet an API type name.

Do
toast.warning('3 saved viewpoints have '
              'the same name')
Don't
toast.warning('3 SavedViewpoint instances '
              'collided on DisplayName')

Copy rules#

  • Sentence case. "Group clashes", not "Group Clashes".
  • No em dashes or en dashes. A comma, a colon, a semicolon, or two sentences. This is a hard rule and it applies to every string a user can read.
  • No emoji, anywhere in the UI. Not in toasts, not in button labels, not in the output window.
  • Plain verbs. "Delete", "Rename", "Export". Not "Proceed", not "Execute", never "Utilize".
  • Errors say the fix. "Could not read the mapping file" plus the path beats "An error occurred", every time.
  • Count things. "Renamed 412 items" beats "Renamed items". The number is what the user actually wanted to know.
  • No apology, no exclamation. State what happened.
  • Say what the user sees. Viewpoints, selection sets, clash tests, search sets. Not ModelItem, not SavedViewpoint.

The same result, twice#

Fails the slop test Passes CLASH RESULTS Great news, your model improved! 412 clashes Recommended fix Level 03 duct vs beam Level 04 tray vs wall Hover a row for options Centered rows, tint washes, pill-soup, a "Recommended" badge, all-caps heading, hover-only actions, cheerful copy, and no count you can compare row to row. Clash summary 412 clashes in 2 groups Level 03 duct vs beam 316 Level 04 tray vs wall 96 One left edge, numbers right-aligned and tabular, neutral selection with an accent rail, one 28px readout as the signature moment, and nothing else raising its voice.
The same 412 clashes. The version on the right is quieter, denser, and tells you more, which is the entire argument for the system.

Before you ship#

Run your tool against this list. It takes two minutes and it catches almost everything.

  • Every exit path shows something, and cancel shows nothing.
  • Nothing selected, no results and no active document each have a message that says what to do next.
  • Predictable failures are caught and reported as a plain sentence with the fix.
  • Anything longer than a second shows counted progress; anything longer than several seconds can be cancelled.
  • A bulk edit is one undo step.
  • No print() for a one-line status.
  • No window opened for a single sentence.
  • The accent appears in three places at most, and the selection is neutral.
  • Every control is reachable by Tab and shows a focus ring.
  • Disabled controls look disabled and say why in a caption.
  • The tool is readable in both themes; you checked with is_dark_theme() both ways.
  • Every string is sentence case, free of emoji, and free of em dashes.
  • No string names an API type the user has never heard of.
  • A screenshot of it would not look out of place beside the rest of the ribbon.