Добавлен новый модуль виджетов.

Реализована механика работы с виджетами по нажатию Tab.
Новый модуль кнопки.
This commit is contained in:
Alexander Zhirov 2026-01-08 23:37:13 +03:00
parent f6150c9b5f
commit 59d5650285
Signed by: alexander
GPG key ID: C8D8BE544A27C511
4 changed files with 256 additions and 0 deletions

View file

@ -13,3 +13,7 @@ public import ncui.engine.screen;
// Вспомогательная библиотека
public import ncui.lib.logger;
// Виджеты
public import ncui.widgets.container;
public import ncui.widgets.button;

View file

@ -0,0 +1,79 @@
module ncui.widgets.button;
import ncui.widgets.widget;
import ncui.core.window;
import ncui.core.event;
import ncui.engine.screen;
import ncui.engine.action;
// Опциональное действие, выполняемое при нажатии на кнопку.
alias OnClick = ScreenAction delegate();
final class Button : IWidget
{
private:
// Кнопка по умолчанию активна.
bool _enabled = true;
// Опциональная функция обратного вызова, при нажатии на кнопку.
OnClick _onClick;
// Координаты начала рисования кнопки.
int _y;
int _x;
// Надпись кнопки.
string _text;
public:
this(int y, int x, string text, OnClick onClick = null)
{
_y = y;
_x = x;
_text = text;
_onClick = onClick;
}
override @property bool focusable()
{
return true;
}
override @property bool enabled()
{
return _enabled;
}
override void render(Window window, ScreenContext context, bool focused)
{
window.put(_y, _x, focused ? "* " : "[ ");
window.put(_y, _x + 2, _text);
window.put(_y, _x + 2 + cast(int) _text.length, focused ? " *" : " ]");
}
override ScreenAction handle(ScreenContext context, KeyEvent event)
{
if (!_enabled)
return ScreenAction.none();
if (isEnter(event) || isSpace(event))
{
if (_onClick !is null)
return _onClick();
return ScreenAction.none();
}
return ScreenAction.none();
}
void onClick(OnClick callback)
{
_onClick = callback;
}
void setText(string text)
{
_text = text;
}
void setEnabled(bool enabled)
{
_enabled = enabled;
}
}

View file

@ -0,0 +1,130 @@
module ncui.widgets.container;
import ncui.widgets.widget;
import ncui.core.window;
import ncui.core.event;
import ncui.engine.screen;
import ncui.engine.action;
/*
* WidgetContainer контейнер виджетов: хранит список, управляет фокусом и
* раздаёт ввод/отрисовку.
*
* - render(): вызывает render() у каждого виджета.
* - handle():
* * Tab -> focusNext(+1) и возвращает ScreenAction.none()
* * иначе: если фокус валиден -> передаём событие текущему виджету
*
* Правила фокуса:
* - нельзя поставить фокус на виджет, если он:
* * не focusable
* * или disabled (enabled == false)
*/
final class WidgetContainer
{
private:
// Список дочерних виджетов.
IWidget[] _children;
// Индекс сфокусированного виджета или -1, если фокуса нет.
int _focus = -1;
public:
// Добавить виджет в контейнер.
void add(IWidget widget)
{
// Индекс элемента ДО добавления.
const int index = cast(int) _children.length;
_children ~= widget;
// Если фокуса ещё нет — дать первому фокусируемому/включенному.
if (_focus < 0 && widget.focusable && widget.enabled)
_focus = index;
}
// Текущий индекс фокуса (или -1).
@property int focusIndex() const
{
return _focus;
}
// Установить фокус на виджет по индексу.
bool setFocus(int index)
{
if (index < 0)
return false;
if (cast(size_t) index >= _children.length)
return false;
auto widget = _children[index];
if (!widget.focusable || !widget.enabled)
return false;
_focus = index;
return true;
}
// Переключить фокус на следующий/предыдущий доступный виджет.
bool focusNext(int delta = +1)
{
// Если нет элементов для фокуса.
if (_children.length == 0)
return false;
const int count = cast(int) _children.length;
// Точка начала текущего элемента.
int index = _focus < 0 ? 0 : _focus;
for (int step = 0; step < count; ++step)
{
index += delta;
if (index < 0)
{
index = count - 1;
}
if (index >= count)
{
index = 0;
}
auto widget = _children[index];
if (!widget.focusable || !widget.enabled)
{
continue;
}
_focus = index;
return true;
}
return false;
}
// Отрисовка виджетов.
void render(Window window, ScreenContext context)
{
foreach (i, child; _children)
{
child.render(window, context, i == _focus);
}
}
// Обработка ввода для контейнера.
ScreenAction handle(ScreenContext context, KeyEvent event)
{
// Tab — переключение фокуса внутри окна.
if (isTab(event))
{
focusNext(+1);
return ScreenAction.none();
}
// Если фокуса нет или он вышел за границы.
if (_focus < 0 || _focus >= _children.length)
return ScreenAction.none();
// Иначе отдать ввод текущему виджету.
return _children[_focus].handle(context, event);
}
}

View file

@ -0,0 +1,43 @@
module ncui.widgets.widget;
import ncui.core.window;
import ncui.core.event;
import ncui.engine.screen;
import ncui.engine.action;
/**
* Базовый интерфейс виджета.
*/
interface IWidget
{
// Можно ли на него поставить фокус.
@property bool focusable();
// Активен ли виджет.
@property bool enabled();
// Отрисовка.
void render(Window window, ScreenContext context, bool focused);
// Ввод.
ScreenAction handle(ScreenContext context, KeyEvent event);
}
// Обработка стандартных нажатий клавиш.
bool isTab(KeyEvent ev)
{
return ev.isChar && ev.ch == '\t';
}
bool isEnter(KeyEvent ev)
{
if (ev.isChar && (ev.ch == '\n' || ev.ch == '\r')) return true;
if (ev.isKeyCode && cast(int)ev.ch == 343) return true; // KEY_ENTER часто 343
return false;
}
bool isSpace(KeyEvent ev)
{
return ev.isChar && ev.ch == ' ';
}