Core · guide
Level indicator
LevelIndicator shows how much of a capacity is in use, and whether that is a problem. ProgressBar draws nearly the same picture and means something else — a job, which only ever goes up and whose full state is the good one.
Edit this page on GitHubDocuments
What it is
LevelIndicator is a readout for a capacity: a disk, a battery, a memory pool, a budget.
<LevelIndicator Maximum="512" Value="446" Warning="384" Critical="461" />The reading is Value between Minimum and Maximum, as for every other
range control. What this one adds is two lines drawn across
that range — Warning and Critical — and a Level that says which side of them the reading is on.
What it is for
Doc 49 § 7.1 ranks it sixth of the controls this set was missing, and the reason is that
ProgressBar was the only readout in it. The two draw nearly the same picture and mean opposite
things:
ProgressBar | LevelIndicator | |
|---|---|---|
| what the number is | how far through a job | how much of a capacity is in use |
| which way it moves | up, and then it is finished | both, for ever |
| what full means | done | possibly the thing you are worried about |
| what a reader hears | progressbar | meter |
⚠ The accessible role is the substance of that table, not the colour. A screen reader told
"progress bar, eighty-seven per cent" about a disk has been told a job is nearly done. LevelIndicator
reports AccessibleRole.Meter — ARIA's meter, which exists for
exactly this distinction — and announces the reading rather than a fraction, because a meter's
bounds are the capacity and "446" is the number the listener wanted, not "0.87".
Using it
Which direction is bad
⚠ With two thresholds, their order is the direction. A disk gets worse as it fills; a battery gets worse as it empties. Both are level indicators, and with both lines set the control tells them apart from the numbers alone:
<!-- A disk. Critical is above Warning, so a bigger reading is a worse one. --><LevelIndicator Value="@Used" Warning="0.7" Critical="0.9" /><!-- A battery. Critical is below Warning, so a smaller reading is a worse one. --><LevelIndicator Value="@Charge" Warning="0.3" Critical="0.1" />Leave either threshold unset — they default to float.NaN — and nothing on that line ever fires.
⚠ One line has no order, so with one line you say which way is worse. Left to itself a single
threshold reads upward, because a lone line on a capacity is a ceiling — and so a battery given only
Critical="0.1" reads critical at a full charge. Direction is how it says otherwise:
<!-- One line, and a smaller reading is the worse one. --><LevelIndicator Value="@Charge" Critical="0.1" Direction="Falling" />Direction is Inferred unless it is set, which is the pair's order when there are two lines and
upward when there is one, so an indicator that never mentions it behaves exactly as it always has.
A stated direction is obeyed even against the pair. That is deliberate: a falling direction over a
disk's 0.7/0.9 reads critical at every ordinary reading, which is loud, whereas a property that
was quietly ignored whenever two lines were set would be the silent kind of wrong.
What it looks like
The level reaches the stylesheet as a class, so the theme owns the colours:
level-indicator.warning { --fill-color: var(--warning); }level-indicator.critical { --fill-color: var(--danger); }ControlTheme.vcss writes exactly those two rules. Warning and danger are not the same word: danger
says something has gone wrong or is about to be destroyed; warning says a reading is heading somewhere
and there is still time.
⚠ --warning is new to the control set's palette and not new to the tree. The editor has declared
its own --warning for as long as it has had a console — #9a6200 dark, #d99a3c light — and the
editor's declaration wins wherever the editor is running, because ControlTheme's palette sits in the
base layer and EditorTheme is wholly in components. Layer beats load order, so this holds
whichever sheet is installed first. What the control set adds is the default underneath: #a26507, for
an application that installs only the control set. Write a warning-coloured rule against the token
rather than against either literal.
Set Segments above zero to draw blocks instead of a bar — signal strength, a rating, a battery with
cells:
<LevelIndicator Value="@Signal" Segments="4" />⚠ The lit block count rounds rather than truncating. Four blocks at seven-tenths is three, not
two. A truncating version shows its last block only at the very top of the range, so a meter resting
anywhere near a boundary reads one block low for as long as it rests there. The gap between blocks is
--segment-gap and is taken out of each block rather than added between them, so a segmented
indicator and a continuous one of the same width end in the same place.
Set TickCount to draw a ruler across the bar — AppKit's numberOfTickMarks, counted the same way, so
five ticks mark the quarters with the first and last at the ends:
<LevelIndicator Value="@Used" TickCount="5" />⚠ The ticks are drawn over the fill, not under it, the opposite of a Slider. A slider's thumb
stands where the value is; a level indicator has no thumb, so the marks are read against the fill and a
fill that covered them would hide every tick the reading has passed. The last tick sits on the bar's
last pixel column rather than the one after it, which would be outside the element and clipped.
From C#
var disk = panel.Add<LevelIndicator>();disk.Maximum = 512f;disk.Warning = 384f;disk.Critical = 461f;disk.Value = 446f;if (disk.Level == LevelReading.Critical) { // 446 of 512 is past the critical line, so this branch is taken.}Level is computed from the three numbers every time it is read rather than stored, so it cannot come
to disagree with them — the failure a cached level has is that it is right until somebody moves a
threshold and never touches the value again.
Examples
A disk-space row in a settings panel, a signal strength, and a battery — three readings, three directions, one control:
<KeyValueList> <key-value-row> <key-value-key>Disk</key-value-key> <key-value-value> <LevelIndicator Maximum="512" Value="@Used" Warning="384" Critical="461" /> </key-value-value> </key-value-row> <key-value-row> <key-value-key>Signal</key-value-key> <key-value-value><LevelIndicator Value="@Signal" Segments="4" /></key-value-value> </key-value-row> <key-value-row> <key-value-key>Battery</key-value-key> <key-value-value> <!-- Critical below Warning, so this one reads downward. --> <LevelIndicator Value="@Charge" Warning="0.3" Critical="0.1" /> </key-value-value> </key-value-row></KeyValueList>ControlTheme.vcss already stretches a level-indicator inside a key-value-value, alongside every
other control that belongs in that half of a row.
What it deliberately does not do
- It is not focusable and takes no input. A readout is not operated;
ProgressBarmakes the same choice for the same reason. Bind it to a value and let something else change that value. - It has no text. A number beside a bar is a
LabeledContentor aKeyValueListrow with this in it, and the words belong to the application — "446 GB of 512 GB" is not a sentence this assembly can write without knowing what is being measured. - It does not warn about itself. Passing
Criticaladds a class and changes an announcement; it raises no event and shows no dialog. What to do about a full disk is the application's decision.
See also
- Accessibility — the role tree these readings are announced through, and why a
meteris not aprogressbar. - Key-value list — where a reading usually sits, beside the word for what it is.
- Labeled content — the same, for a form row rather than a table of facts.
- Gauge — the same reading, thresholds and levels, drawn as a dial.