PIPKIT · Components

Статус-бар, виджеты, списки, плитки и графики.

Эта страница повторяет компоненты и готовые UI-паттерны из API.md: status bar, widgets, list/tile menu и graph subsystem.

12. Статус-бар

12.1. Build-time флаги

  • PIPGUI_STATUS_BAR
  • 1 включает код статус-бара
  • 0 вырезает runtime-реализацию, публичные методы остаются no-op
  • PIPGUI_DEBUG_METRICS
  • 1 включает debug-режим: библиотека рисует диагностический текст в статус-баре (FPS/время кадра/память и т.п.)
  • 0 выключено (по умолчанию)

Обычно эти флаги задаются в include/config.hpp.

12.2. Включение

ui.configStatusBar()
    .height(18)
    .pos(Top)
    .style(Blur);

Позиции:

  • Top
  • Bottom

Стили:

  • Solid — обычная непрозрачная полоса; layout резервирует под неё высоту
  • Blur — блюр-полоса поверх контента; layout не должен откусывать под неё safe area

12.3. Текст

ui.setStatusBarText()
    .left("PipGUI")
    .center("12:34")
    .right("Wi-Fi");

12.4. Батарея

ui.setStatusBarBattery(87, Numeric);
ui.setStatusBarBattery(100, Bar);
ui.setStatusBarBattery(-1, Hidden);

BatteryStyle:

  • Hidden
  • Numeric
  • Bar
  • WarningBar
  • ErrorBar

12.5. Кастомная дорисовка

void statusBarCustom(GUI &ui, int16_t x, int16_t y, int16_t w, int16_t h)
{
    ui.drawLine()
        .from(x, y + h - 1)
        .to(x + w, y + h - 1)
        .color(ui.rgb(70, 70, 70))
        .draw();
}

ui.setStatusBarCustom(statusBarCustom);

Вспомогательные методы:

int16_t h = ui.statusBarHeight();
ui.updateStatusBar();
ui.renderStatusBar();

Что важно:

  • statusBarHeight() возвращает ненулевую высоту только для Solid
  • при Blur helper возвращает 0, потому что layout не должен резервировать fixed-height safe area под blur-панель

12.6. Иконки слотов

Можно повесить отдельную иконку в левый, центральный или правый слот статус-бара:

ui.setStatusBarIcon()
    .side(Left)
    .icon(warning);

ui.setStatusBarIcon()
    .side(Center)
    .icon(error)
    .color(ui.rgb(255, 80, 80))
    .size(16);

ui.setStatusBarIcon()
    .side(Right)
    .icon(warning)
    .color(ui.rgb(255, 220, 120))
    .size(14);

Параметры:

  • первый аргумент - слот: Left, Center или Right
  • второй аргумент - IconId
  • color необязателен; если не задан, берётся foreground-цвет статус-бара
  • sizePx необязателен; если 0, размер подбирается автоматически от высоты панели

Удаление иконки:

ui.clearStatusBarIcon(Left);
ui.clearStatusBarIcon(Center);
ui.clearStatusBarIcon(Right);

Поведение:

  • иконки появляются и исчезают с короткой fade-анимацией
  • левая иконка живёт в одном block-е с левым текстом
  • центральная иконка центрируется вместе с центральным текстом как одна группа
  • правая иконка живёт в правом block-е вместе с правым текстом

13. Виджеты

13.1. Scroll dots

ui.drawScrollDots()
    .pos(center, 220)
    .count(5)
    .activeIndex(2)
    .activeColor(ui.rgb(0, 87, 250))
    .inactiveColor(ui.rgb(60, 60, 60)) 
    .radius(3)
    .spacing(14);

Есть и updateScrollDots() с теми же параметрами.

Что задают параметры:

  • count(...) - общее число страниц/точек
  • activeIndex(...) - текущая активная страница
  • radius(...) - базовый радиус точки
  • spacing(...) - шаг между центрами соседних точек

Поведение:

  • при count > 7 включается оконный режим: показывается компактное окно точек с taper по краям

13.2. Buttons

Обычная отрисовка:

ui.drawButton()
    .label("Save")                       // текст внутри кнопки
    .pos(center, 180)                    // центр по X, координата Y
    .size(120, 40)                       // ширина и высота кнопки
    .baseColor(ui.rgb(0, 120, 255))      // основной цвет кнопки
    .radius(10);                         // радиус

Обновление с состоянием кнопки:

ui.updateButton()
    .label("Save")                       // текст внутри кнопки
    .pos(center, 180)                    // центр по X, координата Y
    .size(120, 40)                       // ширина и высота кнопки
    .baseColor(ui.rgb(0, 120, 255))      // основной цвет кнопки
    .radius(10)                          // радиус
    .icon(warning)                       // иконка внутри кнопки
    .mode(true, false)                   // enabled, loading
    .down(isDown)                        // текущее физическое нажатие для press-анимации
;

Кнопка с прогрессом:

ui.updateButton()
    .label("Updating 56%")               // текст внутри кнопки
    .pos(center, 180)                    // центр по X, координата Y
    .size(170, 38)                       // ширина и высота кнопки
    .baseColor(ui.rgb(24, 24, 24))       // базовый цвет корпуса кнопки
    .fillColor(ui.rgb(0, 120, 255))      // цвет progress-заливки
    .value(56)                           // значение встроенного progress: 0..100
    .radius(12)                          // радиус
    .icon(battery_l1);                   // иконка внутри кнопки

drawButton() и updateButton() используют один и тот же API. Для обычных статичных экранов достаточно drawButton(). Для анимируемой или интерактивной кнопки используй updateButton().

Если заданы и текст, и иконка - иконка рисуется слева от текста как единый центрированный блок. Если текст пустой, иконка рисуется по центру кнопки. Если задан value(...), кнопка рисует встроенный progress-fill под текстом и иконкой. loading и progress одновременно не используются: progress-режим приоритетнее.

13.3. Toggle switch

Снаружи нужен только обычный bool:

bool wifiEnabled = false;
bool changed = false;

Отрисовка:

ui.updateToggleSwitch()
    .pos(center, 140)
    .size(78, 36)
    .value(wifiEnabled)                   // текущий bool; библиотека сама обновит его при нажатии
    .pressed(btn.wasPressed())            // событие нажатия этого кадра
    .changed(changed)                     // сюда вернется true, если значение переключилось
    .enabled(!wifiBusy)                   // можно временно отключить ввод, пока идет внешняя операция
    .activeColor(ui.rgb(21, 180, 110))    // цвет включенного track
    .inactiveColor(ui.rgb(46, 46, 46))    // цвет выключенного track
    .knobColor(0xFFFF);                   // цвет бегунка

drawToggleSwitch() и updateToggleSwitch() используют один и тот же fluent API. Разница только в режиме вывода:

  • drawToggleSwitch() - обычная отрисовка
  • updateToggleSwitch() - локальный dirty-update

Если хочется просто показать состояние без ввода, можно не передавать pressed(...) и changed(...):

ui.drawToggleSwitch()
    .pos(center, 140)
    .size(78, 36)
    .value(wifiEnabled)
    .activeColor(ui.rgb(21, 180, 110));

13.4. Slider

Слайдер подходит для настроек вроде громкости, яркости и подобных значений.

ui.updateSlider()
    .pos(center, 114)                     // центр по X, координата Y
    .size(186, 24)                        // ширина и высота трека
    .bind(value)                          // привязка переменной; библиотека обновляет ее сама
    .activeColor(ui.rgb(0, 87, 250))      // цвет заполненной части
    .inactiveColor(ui.rgb(36, 36, 36))    // цвет неактивной части трека
    // .enabled(false)                    // опционально: показать slider без реакции на ввод
    .thumbColor(0xFFFF);                  // цвет бегунка

drawSlider() и updateSlider() используют один и тот же fluent API. Для интерактивного сценария нужен updateSlider(). Ввод Next/Prev slider берет сам из последнего pollInput(...).

Поведение:

  • удержание кнопки сначала двигает значение обычным шагом, потом ускоряет и частоту, и величину шага;
  • по умолчанию бегунок белый, трек темнее активной части.

13.5. Прогресс

ui.drawProgress()
    .pos(20, 220)                        // левый верхний угол
    .size(180, 16)                       // ширина и высота
    .value(65)                           // значение progress: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .radius(8)                           // скругление краев
    .anim(Shimmer);                      // тип анимации progress

Для локального обновления без полной перерисовки есть updateProgress():

ui.updateProgress()
    .pos(20, 220)                        // та же область, которую нужно обновить
    .size(180, 16)                       // тот же размер
    .value(65)                           // новое значение: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .radius(8)                           // скругление краев
    .anim(Shimmer);                      // тип анимации progress

Текст у линейного прогресса задаётся прямо на самом progress:

ui.drawProgress()
    .pos(20, 246)                            // левый верхний угол
    .size(180, 14)                           // ширина и высота
    .value(65)                               // значение progress: 0..100
    .baseColor(ui.rgb(20, 20, 20))           // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))          // цвет заполненной части
    .label("Downloading", Left)              // текст и его выравнивание
    .labelColor(ui.rgb(255, 255, 255))       // цвет label
    .percent(Right)                          // показать текущее значение как процент и задать выравнивание
    .percentColor(ui.rgb(200, 200, 200));    // цвет процента

Текст поддерживается только у линейного progress; у drawCircleProgress() его нет.

Для label(...) и percent(...) доступны:

  • Left - прижать текст к левому краю progress
  • Center - выровнять текст по центру progress
  • Right - прижать текст к правому краю progress

anim(...) у progress поддерживает:

  • None - progress статичный, без анимации
  • Shimmer - по заполненной части идет мягкий движущийся блик

13.6. Круговой прогресс

ui.drawCircleProgress()
    .pos(center, 140)                    // центр кольца
    .radius(34)                          // внешний радиус
    .thickness(8)                        // толщина кольца
    .value(72)                           // значение progress: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .anim(None);                         // тип анимации progress

Локальное обновление:

ui.updateCircleProgress()
    .pos(center, 140)                    // тот же центр
    .radius(34)                          // тот же внешний радиус
    .thickness(8)                        // та же толщина кольца
    .value(72)                           // новое значение: 0..100
    .baseColor(ui.rgb(30, 30, 30))       // цвет пустой части
    .fillColor(ui.rgb(0, 120, 255))      // цвет заполненной части
    .anim(None);                         // тип анимации progress

13.7. Drum roll

Горизонтальный:

ui.drawDrumRoll()
    .pos(20, 60)                            // левый верхний угол
    .size(200, 40)                          // ширина и высота области
    .options(16, "Low", "Medium", "High")   // шрифт и список опций; количество считается автоматически
    .selected(1)                            // текущий выбранный индекс
    .fgColor(ui.rgb(255, 255, 255))         // цвет текста
    .bgColor(ui.rgb(0, 0, 0));              // цвет фона

Вертикальный:

ui.drawDrumRoll()
    .pos(220, 100)                          // левый верхний угол
    .size(70, 90)                           // ширина и высота области
    .options(16, "Low", "Medium", "High")   // шрифт и список опций; количество считается автоматически
    .selected(1)                            // текущий выбранный индекс
    .fgColor(ui.rgb(255, 255, 255))         // цвет текста
    .bgColor(ui.rgb(0, 0, 0))               // цвет фона
    .vertical();                            // включить вертикальный режим

14. Списки и плитки

14.1. Списочное меню

Прямо в SCREEN(...):

SCREEN(ScreenMainMenu, 1)
{
    ui.clear(0x0000);

    ui.updateList()
    .items(
        listItem("Settings", "Device configuration", ScreenSettings),
        listItem("About",    "Firmware info",        ScreenAbout),
        listItem("Restart",  "Reboot device",        ScreenRestart))
        .inactive(ui.rgb(12, 12, 12))      // цвет обычной карточки
        .active(ui.rgb(0, 120, 255))       // цвет активной карточки
        // .checked(1)                        // опционально: справа рисовать галочку у этого пункта
        .radius(8)                         // радиус карточек
        .cardSize(0, 0)                    // 0,0 = размер подберётся автоматически
        .mode(Cards);                      // режим списка
}

Режимы списка:

  • Cards
  • Plain

Поведение:

  • короткое отпускание NEXT переключает пункт вперёд;
  • короткое отпускание PREV переключает пункт назад;
  • 2-button режим: удержание NEXT открывает targetScreen выбранного пункта;
  • 3-button режим: короткое нажатие SELECT открывает targetScreen выбранного пункта;
  • удержание PREV возвращает на предыдущий экран из navigation-history.

14.2. Плиточное меню

Прямо в SCREEN(...):

SCREEN(ScreenTiles, 2)
{
    ui.updateTile()
    .items(
        tileItem("Main",     "Главный экран", ScreenHome),
        tileItem("Settings", "Настройки",     ScreenSettings),
        tileItem("Info",     "Инфо",          ScreenInfo),
        tileItem("Graph",    "Графики",       ScreenGraph))
        .inactive(ui.rgb(16, 16, 16))     // обычная плитка
        .active(ui.rgb(0, 120, 255))      // активная плитка
        .radius(12)                       // радиус плитки
        .spacing(10)                      // расстояние между плитками
        .columns(2)                       // количество колонок в обычной сетке
        .tileSize(100, 70)                // желаемый размер плитки
        .mode(TextSubtitle);              // контент плитки
}

Поведение:

  • короткое отпускание NEXT переключает плитку вперёд;
  • короткое отпускание PREV переключает плитку назад;
  • 2-button режим: удержание NEXT открывает targetScreen выбранной плитки;
  • 3-button режим: короткое нажатие SELECT открывает targetScreen выбранной плитки;
  • удержание PREV возвращает на предыдущий экран из navigation-history.

Режимы плитки:

  • TextOnly
  • TextSubtitle

14.3. Кастомная раскладка плиток

Если обычной сетки мало, можно задать свою понятную раскладку:

ui.updateTile()
    .grid(2, 3)                                      // сетка: 2 колонки, 3 строки
    .tile("Main", "Главный экран", ScreenHome)
    .at(0, 0)                                        // колонка 0, строка 0
    .span(2, 1)                                      // ширина 2 клетки, высота 1 клетка
    .tile("Settings", "Настройки", ScreenSettings)
    .at(0, 1)                                        // колонка 0, строка 1
    .tile("Info", "Инфо", ScreenInfo)
    .at(1, 1)                                        // колонка 1, строка 1
    .tile("Graph", "Графики", ScreenGraph)
    .at(1, 2)                                        // колонка 1, строка 2

Что важно:

  • grid(cols, rows) включает кастомную сетку
  • tile(...) добавляет очередную плитку
  • at(col, row) ставит последнюю добавленную плитку в нужную клетку
  • span(cols, rows) растягивает последнюю добавленную плитку на несколько клеток
  • в этом режиме columns(...) уже не влияет на раскладку

15. Графики

15.1. Фон и сетка:

ui.drawGraphGrid()
    .pos(10, 50)
    .size(220, 120)
    .radius(8)
    .direction(LeftToRight)           // задаёт режим движения и раскладки данных внутри этой graph-area
    .bgColor(ui.rgb(10, 10, 10))      // цвет сетки вычисляется автоматически из bgColor()
    .speed(1.0f);
  • drawGraphGrid() / updateGraphGrid() должны использовать тот же direction(...), в котором потом рисуются линии этого графа

Направления:

  • LeftToRight - новые точки приходят справа, старая история уезжает влево
  • RightToLeft - новые точки приходят слева, старая история уезжает вправо
  • Oscilloscope - фиксированное окно по всей ширине графика без rolling-shift; точки раскладываются по видимому буферу как осциллограф

Для локального dirty-redraw доступен тот же fluent через updateGraphGrid():

ui.updateGraphGrid()
    .pos(10, 50)
    .size(220, 120)
    .radius(8)
    .direction(LeftToRight)
    .bgColor(ui.rgb(10, 10, 10))
    .speed(1.0f);

15.2. Линия графика:

ui.drawGraphLine()
    .line(0)
    .value(sensorValue)
    .thickness(2)                   // задаёт толщину линии графика; по умолчанию `1`
    .color(ui.rgb(0, 255, 140))
    .range(0, 100);
    .scale(true);                   // вкл/выкл автоматический диапазон по данным для этой graph-area
  • drawGraphLine() добавляет новую точку в уже настроенный график
  • updateGraphLine() подходит для in-place обновления, когда графику нужно самому зачистить и перерисовать нужную область

15.3. Пакетная отрисовка готового массива:

int16_t samples[] = {10, 15, 12, 18, 20, 17};

ui.drawGraphSamples()
    .line(0)
    .samples(samples, 6)
    .thickness(2)
    .color(ui.rgb(0, 255, 140))
    .range(0, 100);
  • drawGraphSamples() рисует переданный массив сразу, не накапливает внутреннюю историю точек. Для streaming-режима с накоплением используйте drawGraphLine()
  • updateGraphSamples() использует тот же API, но подходит для локального in-place обновления

Для Oscilloscope эти настройки задаются прямо у сетки:

ui.drawGraphGrid()
    .pos(center, center)
    .size(200, 170)
    .direction(Oscilloscope)
    .scope(2000, 100)                  // частота входных samples и длительность окна в мс
    .visible(0);

Что важно по lifecycle:

  • drawGraphGrid() задаёт активную область графика и должен вызываться в screen-callback этого экрана
  • buffered-график (drawGraphLine()) живёт только пока экран реально рисует граф в текущих кадрах
  • если экран перестал вызывать graph API или вы ушли на другой screen, внутренние буферы графа освобождаются
  • при возврате граф начинает собирать историю заново

Что важно по режимам:

  • LeftToRight и RightToLeft используют rolling-history
  • Oscilloscope использует фиксированное окно видимых samples
  • если visible(0), окно для Oscilloscope вычисляется из rateHz * timebaseMs

15.4. Пауза графиков (freeze)

В 3-button режиме, библиотека поддерживает заморозку графика:

  • короткое нажатие Select на экране, где рисуется граф, переключает pause/resume только для графика этого экрана;
  • во время паузы новые значения/массивы для графиков игнорируются, а на экране остаётся последний отрисованный кадр графа.
bool paused = ui.graphPaused();                 // текущее состояние
bool toggled = ui.GraphPauseToggled();          // одноразовое событие (true один раз после переключения)

Важно:

  • механизм включён только когда реально есть 3-я кнопка (3-button режим);

Code copied