# Панель управления (ПУ)
## Основные понятия
- Виджет - компнент графического интерфейса (кнопка, поле ввода..)
- Панель управления (ПУ) - экран приложения, на котором отображаются виджеты
- Билдер - функция в программе для устройства, в которой происходит сборка панели управления из виджетов

## Билдер
```cpp
// билдер
void build(gh::Builder& b) {
}

void setup() {
    hub.onBuild(build); // подключаем билдер
}
```
Библиотека будет вызывать эту функцию для отправки виджетов в приложение, для обработки действий с приложения и для чтения значений с виджетов. Особенности:
- В билдере не должно быть задержек `delay()` и прочего блокирующего или долго выполняющегося кода! Билдер вызывается в обработчике ответа приложению, во время выполнения билдера приложение ждёт ответа
- Во время выполнения билдера собирается строка с ответом приложению, поэтому внутри билдера рекомендуется *максимально ограничить* действия со `String` строками и динамическим выделением памяти (`malloc()`, `new`) - они будут приводить к фрагментации оперативной памяти

## Типы данных
Для удобства в библиотеке используются "универсальные" типы данных. Компилятор сам понимает, что передали в функцию:
- `AnyText` - строка в любом виде (`"строка"`, `F("строка")`, `char*`, `String`)
- `AnyValue` - любой целочисленный и дробный тип данных + строки в любом виде как у AnyText
- `AnyPtr` - указатель (адрес) переменной любого типа (String, char*, все числа) + системные (Colors, Flags, Pos, Button, Log)

А также:
- `Pairs` - база данных, библиотека [Pairs](https://github.com/GyverLibs/Pairs). Можно передать как экземпляр `Pairs`, так и `PairsFile`

## Виджеты
Создание и настройка практически всех виджетов производится по следующей схеме: вызывается виджет, у него могут быть **личные параметры** в скобках, затем через точку можно применить **общие параметры**:
```cpp
void build(gh::Builder& b) {
    b.Widget();         // без параметров
    b.Widget(...);      // личные параметры
    b.Widget(...).param1(...).param2(...)...;   // + дополнительные параметры
}
```

## Личные параметры
Все виджеты можно разделить на две группы: виджеты с переменной и виджеты со значением.

### С переменной
- `ptr` - к такому виджету можно подключить переменную любого типа, библиотека будет брать с неё значение при отправке виджета, а также записывать отправленное из приложения новое значение. К некоторым виджетам можно подключить переменную только определённого типа, см. документацию
- `name` - уникальное имя виджета. Нужно для отправки обновлений или чтения из Pairs
```cpp
Widget(AnyPtr ptr = nullptr);
Widget_(AnyText name = "", AnyPtr ptr = nullptr);
```

Примеры:
```cpp
int i;
String s;
Pairs p;

void build(gh::Builder& b) {
    b.Input();              // без параметров
    b.Input_("inp");        // только имя
    b.Input(&i);            // только переменная
    b.Input_("inp2", &s);   // имя и переменная
    b.Input_("inp3", &p);   // имя и значение из Pairs
}
```

### Со значением
- `text` - значение, которое отображается на виджете
- `name` - уникальное имя виджета. Нужно для отправки обновлений или чтения из Pairs
```cpp
Widget(AnyValue text = "");
Widget_(AnyText name = "", AnyValue text = "");
Widget_(AnyText name = "", Pairs* pairs = nullptr);
```

Примеры:
```cpp
int i;
String s;
Pairs p;

void build(gh::Builder& b) {
    b.Display();                // без параметров
    b.Display_("disp");         // только имя
    b.Display(i);               // только значение
    b.Display_("disp2", s);     // имя и значение
    b.Display_("disp3", &p);    // имя и значение из Pairs
}
```

## Общие параметры
В библиотеке есть общий набор параметров, но виджеты поддерживают не все их одновременно, см. таблицу ниже. Также есть параметры самого блока виджета, они могут применяться к любому виджету.

### Параметры блока
- `size(uint16_t width, uint16_t height = 0)` - Ширина (относительно) и высота (px) виджета
- `label(AnyText str)` - Заголовок виджета
- `noLabel(bool nolabel = true)` - Убрать заголовок виджета
- `suffix(AnyText str)` - Дополнительный заголовок виджета справа
- `noTab(bool notab = true)` - Убрать задний фон виджета
- `square(bool square = true)` - Сделать виджет квадратным
- `disabled(bool disable = true)` - Отключить виджет
- `hint(AnyText str)` - подсказка виджета. Пустая строка - убрать подсказку

### Параметры виджета
| Виджет/параметр | text  | icon  | maxLen | rows  | regex | align | range | unit  | fontSize | color | click | attach |
| --------------- | :---: | :---: | :----: | :---: | :---: | :---: | :---: | :---: | :------: | :---: | :---: | :----: |
| Input           |       |       |   ✔    |       |   ✔   |       |       |       |          |   ✔   |   ✔   |   ✔    |
| InputArea       |       |       |   ✔    |   ✔   |       |       |       |       |          |       |   ✔   |   ✔    |
| Pass            |       |       |   ✔    |       |   ✔   |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Confirm         |   ✔   |       |        |       |       |       |       |       |          |       |   ✔   |   ✔    |
| Prompt          |   ✔   |       |        |       |       |       |       |       |          |       |   ✔   |   ✔    |
| Date            |       |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Time            |       |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| DateTime        |       |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Slider          |       |       |        |       |       |       |   ✔   |   ✔   |          |   ✔   |   ✔   |   ✔    |
| Spinner         |       |       |        |       |       |       |   ✔   |   ✔   |          |       |   ✔   |   ✔    |
| Select          |   ✔   |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Color           |       |       |        |       |       |       |       |       |          |       |   ✔   |   ✔    |
| Switch          |       |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| SwitchIcon      |       |   ✔   |        |       |       |       |       |       |    ✔     |   ✔   |   ✔   |   ✔    |
| Tabs            |   ✔   |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Button          |       |   ✔   |        |       |       |       |       |       |    ✔     |   ✔   |   ✔   |   ✔    |
| Flags           |   ✔   |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Joystick        |       |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Dpad            |       |       |        |       |       |       |       |       |          |   ✔   |   ✔   |   ✔    |
| Title           |       |   ✔   |        |       |       |   ✔   |       |       |    ✔     |   ✔   |       |        |
| Label           |       |   ✔   |        |       |       |   ✔   |       |       |    ✔     |   ✔   |       |        |
| Text            |       |       |        |   ✔   |       |       |       |       |          |       |       |        |
| TextFile        |       |       |        |   ✔   |       |       |       |       |          |       |       |        |
| Display         |       |       |        |   ✔   |       |       |       |       |    ✔     |   ✔   |       |        |
| Image           |       |       |        |       |       |       |       |       |          |       |       |        |
| Log             |       |       |        |   ✔   |       |       |       |       |          |       |       |        |
| LED             |       |       |        |       |       |       |       |       |          |   ✔   |       |        |
| Icon            |       |   ✔   |        |       |       |       |       |       |    ✔     |   ✔   |       |        |
| Gauge           |       |       |        |       |       |       |   ✔   |   ✔   |          |   ✔   |       |        |
| GaugeRound      |       |       |        |       |       |       |   ✔   |   ✔   |          |   ✔   |       |        |
| GaugeLinear     |       |   ✔   |        |       |       |       |   ✔   |   ✔   |          |   ✔   |       |        |
| Table           |       |       |        |       |       |       |       |       |          |       |       |        |

> Также у каждого виджета есть скрытый параметр `value` - он входит в "личные" параметры в скобках функции виджета и является значением виджета

Например:
```cpp
b.Input().label("My input");
b.Button().color(gh::Colors::Red).icon("f6ad");
```

## Подключение переменной
Переменная передаётся по указателю. Для всех типов данных, кроме `char[]` для этого используется оператор `&`, т.к. массив уже является указателем на себя:
```cpp
int num;
String str;
char charr[16];

b.Input(&num);
b.Input(&str);
b.Input(charr);
```

При использовании массивов всё аналогично:
```cpp
int num[10];
String str[10];
char charr[10][16];

for (int i = 0; i < 10; i++) {
    b.Input(&num[i]);
    b.Input(&str[i]);
    b.Input(charr[i]);
}
```

## Обработка действия
С активных виджетов (доступны параметры `click` и `attach`) можно получать сигнал об изменении значения в приложении.

### click
Вызвать параметр `click()` - он вернёт `true` при действии. Вызывать его нужно **последним в цепочке**, т.к. он ломает цепочку (возвращает `bool`):
```cpp
if (b.Button().click()) Serial.println("click 1");
```

### attach bool
Подключить `bool` переменную - флаг:
```cpp
bool flag = 0;

b.Button().attach(&flag);

if (flag) Serial.println("click 2");
```

### attach Flag
Подключить `gh::Flag` переменную - флаг, данный флаг сам сбросится в `false` при проверке!
```cpp
gh::Flag gflag;

b.Button().attach(&gflag);

if (gflag) Serial.println("click 3");
```

### attach handler
Подключить функцию-обработчик:
```cpp
// обработчик кнопки
void btn_cb() {
    Serial.println("click 4");
}

// обработчик кнопки с информацией о билде
void btn_cb_b(gh::Build& b) {
    Serial.print("click 5 from client ID: ");
    Serial.println(b.client.id);
}

void build(gh::Builder& b) {
    // подключить функцию-обработчик
    b.Button().attach(btn_cb);

    // подключить функцию-обработчик с инфо о билде
    b.Button().attach(btn_cb_b);
}
```

## Вёрстка ПУ
В GyverHub виджеты располагаются по сетке, заполнение сетки идёт слева направо сверху вниз. Таким образом нельзя расположить виджет по конкретным координатам или указать конкретные ячейки сетки, также нельзя задать конкретный размер виджета. Это может показаться некоторым "ограничением" свободы действий, но в то же время сильно упрощает сборку и позволяет ПУ выглядеть одинаково хорошо на дисплеях разного размера. Система поддерживает вложенные контейнеры, что даёт возможность собирать интересные ПУ под свои задачи.

### Контейнер ПУ
Основной контейнер ПУ - **вертикальный** (см. ниже):
```cpp
void build(gh::Builder& b) {
    // мы находимся в вертикальном контейнере
    b.Button();
    b.Button();
}
```

<table>
  <tr>
    <td>Button</td>
  </tr>
  <tr>
    <td>Button</td>
  </tr>
</table>

### Горизонтальный контейнер
Виджеты и контейнеры в нём будут располагаться слева направо по мере появления в программе. Есть несколько способов создания контейнера:

#### beginRow/endRow
Использовать функции `beginRow()` и `endRow()` для обозначения контейнера
```cpp
void build(gh::Builder& b) {
    b.beginRow();   // начать контейнер
    b.Button();
    b.Button();
    b.endRow();     // ВАЖНО НЕ ЗАБЫТЬ ЕГО ЗАВЕРШИТЬ
}
```
```cpp
void build(gh::Builder& b) {
    // для удобства можно обернуть контейнер в блок {}
    // функции beginRow() и beginCol() всегда возвращают true
    if (b.beginRow()) {
        b.Button();
        b.Button();

        b.endRow();  // завершить
    }
}
```

#### gh::Row
Создать объект класса `gh::Row(Builder& b, uint16_t width = 1)`. Назвать его можно как угодно, объект сам откроет контейнер и закроет его при выходе за область определения:
```cpp
void build(gh::Builder& b) {
    {
        gh::Row r(b);  // контейнер сам создастся здесь
        b.Button();
        b.Button();
    }  // контейнер сам закроется здесь
}
```

#### GH_ROW
Использовать макрос `GH_ROW(builder, width)`
```cpp
void build(gh::Builder& b) {
    GH_ROW(b, 1,
            b.Button();
            b.Button();
    );
}
```

Все примеры выше дают такой вид:
<table>
  <tr>
    <td>Button</td>
    <td>Button</td>
  </tr>
</table>

### Вертикальный контейнер
Виджеты и контейнеры в нём будут располагаться сверху вниз по мере появления в программе. Для вертикального контейнера справедлив такой же синтаксис: `beginCol()`, `endCol()`, `gh::Col()`, `GH_COL()`. Например:
```cpp
void build(gh::Builder& b) {
    {
        gh::Row r(b);
        b.Button();

        {
            gh::Col c(b);
            b.Button();
            b.Button();
        }
    }
}
```
<table>
<tbody>
  <tr>
    <td rowspan="2">Button</td>
    <td>Button</td>
  </tr>
  <tr>
    <td>Button</td>
  </tr>
</tbody>
</table>

### Ширина виджета
Ширина виджетов задаётся в "долях" - отношении их ширины друг к другу: виджеты займут пропорциональное место во всю ширину контейнера:
```cpp
void build(gh::Builder& b) {
    {
        gh::Row r(b);
        b.Slider().size(3);  // слайдер шириной 3
        b.Button().size(1);  // кнопка шириной 1
        b.Button();          // тоже 1
    }
}
```
<table>
  <tr>
    <td>&nbsp;&nbsp;&nbsp;&nbsp;Slider&nbsp;&nbsp;&nbsp;&nbsp;</td>
    <td>Button</td>
    <td>Button</td>
  </tr>
</table>

Контейнеру тоже можно задать ширину
```cpp
void build(gh::Builder& b) {
    {
        gh::Row r(b);
        {
            // этот контейнер будет в 2 раза шире...
            gh::Row r(b, 2);
            b.Button();
            b.Button();
        }
        {
            // ...чем этот
            gh::Row r(b, 1);
            b.Button();
            b.Button();
        }
    }
}
```
<table>
  <tr>
    <td>&nbsp;&nbsp;&nbsp;Button&nbsp;&nbsp;&nbsp;</td>
    <td>&nbsp;&nbsp;&nbsp;Button&nbsp;&nbsp;&nbsp;</td>
    <td>Button</td>
    <td>Button</td>
  </tr>
</table>


## Динамические виджеты
Билдер собирает виджеты последовательно, поэтому вызов виджетов в цикле также будет работать, позволяя создавать динамические панели управления. Например:
```cpp
void build(gh::Builder& b) {
    {
        gh::Row r(b);
        for (int i = 0; i < 5; i++) {
            if (b.Button().click()) {
                Serial.print("Button #");
                Serial.println(i);
            }
        }
    }
}
```
Выведет 5 кнопок с обработкой кликов на каждой.

Имена также можно задавать вручную, используя сложение строк. Но делать это не рекомендуется:
```cpp
for (int i = 0; i < 5; i++) {
  b.Switch_(String(F("sw")) + i);
}
```

Для подключения переменных в таком случае разумно использовать массив:
```cpp
uint16_t sliders[5];

void build() {
  for (int i = 0; i < 5; i++) {
    b.Slider(&sliders[i]);
  }
}
```

## Меню
В приложении на экране устройства есть выпадающее меню, в него можно добавить свои пункты. Для этого нужно вызвать виджет `Menu()` с указанием списка пунктов через `;`. Виджет можно вызвать только один раз за билдер! Можно сделать это в самом начале:
```cpp
void build(gh::Builder& b) {
    b.Menu("Spinner;Slider;Input");
}
```
> Выбор пункта меню можно отследить через опрос `.click()` как у обычного виджета

> Клик по меню автоматически отправляет запрос на обновление ПУ

Текущий выбранный пункт меню можно прочитать из переменной `GyverHub::menu` (можно изменять вручную) или `gh::Builder::menu()` (только для чтения). По значению пункта меню можно выводить и скрывать виджеты. Самый очевидный способ - обернуть номер пункта меню в `switch`:
```cpp
switch (b.menu()) {
    case 0:
        b.Spinner();
        break;
    case 1:
        b.Slider();
        break;
    case 2:
        b.Input();
        break;
}
```

Это будет работать, но с некоторыми особенностями:
- Если ПУ открыто одновременно в двух и более приложениях и активны разные меню - значения виджетов "пересекутся", например спиннер будет управлять слайдером, так как виджеты с автоматическими именами получат одинаковые имена
- Билдер не сможет прочитать и установить значение виджета из другого пункта меню, даже именованного

Поэтому рекомендуется применять другой механизм выборочной отрисовки виджетов - через `b.show()`. В функцию передаётся `true`, если нужно разрешить вывод виджетов, `false` - чтобы запретить. Вызов без аргумента - по умолчанию `true`:
```cpp
b.show(b.menu() == 0);
b.Spinner();

b.show(b.menu() == 1);
b.Slider();

b.show(b.menu() == 2);
b.Input();

b.show();
```

Теперь билдер "знает" о всех виджетах, которые находятся в ПУ, и раздаёт им корректные автоматические имена. Состояние show просто показывает билдеру, нужно ли выводить виджеты в данный момент, а так все виджеты доступны билдеру для чтения и записи.

## Вкладки
Также есть виджет `Tabs`, с помощью которого можно организовать "вкладки" на ПУ. Логика разбития ПУ на вкладки абсолютно такая же, как при работе с меню, но переменную с номером вкладки нужно создать самостоятельно:

```cpp
void build(gh::Builder& b) {
    static byte tab;
    if (b.Tabs(&tab).text("Spinner;Slider;Input").click()) b.refresh(); // обновить по клику

    // далее switch(tab) или b.show(tab == x)...
}
```

## Сохранение значений
Библиотека не занимается сохранением значений виджетов в энергонезависимой памяти, это полностью отдано под свободу действий программиста. Но есть некоторые инструменты, которые могут с этим помочь.

### Сигнал об изменении
Билдер может просигналить о том, что значение какого то виджета изменилось. Это можно использовать для сохранения данных в памяти, например хранить в структуре и писать в EEPROM (например моя библиотека [EEManager](https://github.com/GyverLibs/EEManager)). Использование EEPROM на ESP крайне не рекомендуется, поэтому лучше использовать библиотеку [FileData](https://github.com/GyverLibs/FileData). Пример:
```cpp
#include <Arduino.h>
#include <FileData.h>
#include <LittleFS.h>

struct Data {
  uint8_t spinner;
  uint16_t slider;
  char str[20];
};
Data data;

FileData data(&LittleFS, "/data.dat", 'A', &mydata, sizeof(mydata));

void build(gh::Builder& b) {
    b.Spinner(&data.spinner);
    b.Slider(&data.slider);
    b.Input(data.str);

    // если что то изменилось - запускаем обновление
    if (b.changed()) data.update();
}

void setup() {
    // .......

    data.read();
}

void loop() {
    // ......

    // сохранение в файл происходит по таймауту здесь
    data.tick();
}
```

### Поддержка Pairs
Также GyverHub нативно поддерживает библиотеку [Pairs](https://github.com/GyverLibs/Pairs) - хранение данных в текстовом виде в формате ключ:значение, а версия `PairsFile` автоматически пишет данные в файл (по таймауту, чтобы снизить износ памяти). Использование очень простое: делается именованный виджет, а вместо переменной подключается экземпляр `PairsFile`:

```cpp
#include <PairsFile.h>
PairsFile data(&LittleFS, "/data.dat", 3000);

void build(gh::Builder& b) {
    b.Input_("input", &data);

    // ещё парочку
    {
        gh::Row r(b);
        b.Slider_("slider", &data);
        b.Spinner_("spinner", &data);
        b.Switch_("switch", &data);
    }

    // выведем содержимое базы данных как текст
    // (Pairs также сама конвертируется в AnyText)
    b.Text(data);
}

void setup() {
    // ........

    // запустить и прочитать базу из файла
    data.begin();
}

void loop() {
    // ........

    // файл сам обновится по таймауту
    data.tick();
}
```

GyverHub будет сам читать значения из базы Pairs и сохранять в неё новые по указанным ключам (имя виджета). При изменении в приложении GyverHub сам инициирует обновление и сохранение файла.