diff --git a/source/ncui/package.d b/source/ncui/package.d index 29c52a8..80730f1 100644 --- a/source/ncui/package.d +++ b/source/ncui/package.d @@ -13,3 +13,7 @@ public import ncui.engine.screen; // Вспомогательная библиотека public import ncui.lib.logger; + +// Виджеты +public import ncui.widgets.container; +public import ncui.widgets.button; diff --git a/source/ncui/widgets/button.d b/source/ncui/widgets/button.d new file mode 100644 index 0000000..c937b7f --- /dev/null +++ b/source/ncui/widgets/button.d @@ -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; + } +} diff --git a/source/ncui/widgets/container.d b/source/ncui/widgets/container.d new file mode 100644 index 0000000..18c4546 --- /dev/null +++ b/source/ncui/widgets/container.d @@ -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); + } +} diff --git a/source/ncui/widgets/widget.d b/source/ncui/widgets/widget.d new file mode 100644 index 0000000..a79f9cd --- /dev/null +++ b/source/ncui/widgets/widget.d @@ -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 == ' '; +}