diff --git a/source/ncui/engine/action.d b/source/ncui/engine/action.d new file mode 100644 index 0000000..4343ed5 --- /dev/null +++ b/source/ncui/engine/action.d @@ -0,0 +1,119 @@ +/** + * Команды, которые экран возвращает движку. + */ +module ncui.engine.action; + +import ncui.engine.screen; + +/** + * Тип команды, которую экран возвращает движку. + * + * `ActionKind` описывает, что именно движок должен сделать после обработки ввода: + * изменить стек экранов, сменить тему, завершить приложение и т.д. + */ +enum ActionKind +{ + // Ничего не делать. + None, + // Добавить новый экран поверх текущего. + Push, + // Заменить верхний экран стека новым. + Replace, + // Удалить один или несколько экранов с вершины стека. + Pop, + // Завершить выполнение UI-цикла. + Quit +} + +/** + * Базовые типы результата экрана. + */ +enum ScreenKind +{ + // Результат отсутствует или не имеет специальной семантики. + None, + // Отмена действия. + Cancel +} + +/** + * Результат работы экрана. + */ +struct ScreenResult +{ + // Общий тип результата + ScreenKind kind; + + static ScreenResult none() + { + return ScreenResult(ScreenKind.None); + } + + static ScreenResult cancel() + { + return ScreenResult(ScreenKind.Cancel); + } +} + +/** + * Действие, которое возвращает экран. + */ +struct ScreenAction +{ + // Тип действия. + ActionKind kind; + // Следующий экран (используется для `Push` и `Replace`). + IScreen next; + // Результат (используется для `Pop`, `Quit`). + ScreenResult result; + /** + * Ничего не делать. + * + * Возвращается, если стек и состояние движка менять не требуется. + */ + static ScreenAction none() + { + return ScreenAction(ActionKind.None, null, ScreenResult.none()); + } + /** + * Добавить новый экран поверх текущего. + * + * Params: + * - screen: создаваемый экран. + */ + static ScreenAction push(IScreen screen) + { + // assert(isPointer!(typeof(result)), "ncuiNotNull expects a function that returns a pointer."); + return ScreenAction(ActionKind.Push, screen, ScreenResult.none()); + } + /** + * Заменить верхний экран стека новым. + * + * Params: + * - screen: создаваемый экран (не должен быть `null`). + */ + static ScreenAction replace(IScreen screen) + { + return ScreenAction(ActionKind.Replace, screen, ScreenResult.none()); + } + /** + * Закрыть верхний экран и передать результат родителю. + * + * Params: + * - result: результат закрываемого экрана. + */ + static ScreenAction pop(ScreenResult result) + { + return ScreenAction(ActionKind.Pop, null, result); + } + /** + * Завершить UI-цикл и вернуть финальный результат наружу. + * + * Params: + * - result: финальный результат приложения (возвращается из `NCUI.run()`). + */ + static ScreenAction quit(ScreenResult result) + { + return ScreenAction(ActionKind.Quit, null, result); + } +} diff --git a/source/ncui/engine/ncui.d b/source/ncui/engine/ncui.d new file mode 100644 index 0000000..5bf4ac4 --- /dev/null +++ b/source/ncui/engine/ncui.d @@ -0,0 +1,107 @@ +/** + * Главный движок: стек экранов + цикл ввода. + */ +module ncui.engine.ncui; + +import core.stdc.stdlib : EXIT_SUCCESS, EXIT_FAILURE, exit; +import std.array : popBack; + +import ncui.lib.logger; +import ncui.core.session; +import ncui.engine.screen; +import ncui.engine.action; + +final class NCUI +{ +private: + // Корневая сессия ncurses. + Session _session; + // Контекст выполнения действующей сессии. + ScreenContext _context; + // Стек экранов. + IScreen[] _stack; + // Флаг активности работы движка. + bool _running; + // Конечный результат выполнения. + ScreenResult _result; + + void apply(ScreenAction action) + { + while (_running && action.kind != ActionKind.None) + { + final switch (action.kind) + { + case ActionKind.Push: + _session.clear(); + _stack ~= action.next; + action = action.next.onShow(_context); + break; + + case ActionKind.Replace: + if (_stack.length != 0) + { + _stack[$ - 1].close(); + _stack.popBack(); + } + _session.clear(); + _stack ~= action.next; + action = action.next.onShow(_context); + break; + + case ActionKind.Pop: + break; + + case ActionKind.Quit: + _result = action.result; + _running = false; + return; + + case ActionKind.None: + break; + } + } + } + +public: + this(const SessionConfig config = SessionConfig.init) + { + try + { + _session = new Session(config); + } + catch (Exception e) + { + error("Failed to initialize the session: ", e.msg); + exit(EXIT_FAILURE); + } + + _context = ScreenContext(_session); + } + + ScreenResult run(IScreen screen) + { + // Пометить работу движка активным. + _running = true; + // Положить первый экран в стек для начала работы. + apply(ScreenAction.push(screen)); + + while (_running && _stack.length != 0) + { + // Взять из стека последний экран. + auto currentScreen = _stack[$ - 1]; + // Ожидать события нажатия клавиш в извлеченном из стека экране. + auto event = _session.readKey(currentScreen.inputWindow()); + // Обработать нажатие клавиши в текущем окне. + auto action = currentScreen.handle(_context, event); + // Обработать возвращенное действие из окна. + apply(action); + } + + // Завершить сессию ncurses. + _session.close(); + + info("Engine successfully stopped"); + + return _result; + } +} diff --git a/source/ncui/engine/screen.d b/source/ncui/engine/screen.d new file mode 100644 index 0000000..400271c --- /dev/null +++ b/source/ncui/engine/screen.d @@ -0,0 +1,86 @@ +/** + * Контракты экранов (screen) и контекст выполнения. + */ +module ncui.engine.screen; + +import ncui.core.session; +import ncui.core.event; +import ncui.core.ncwin; +import ncui.core.window; + +import ncui.engine.action; + +/** + * Контекст выполнения экрана. + */ +struct ScreenContext +{ + Session session; + + this(Session s) + { + session = s; + } +} + +/** + * Базовый интерфейс экрана. + * + * Правила: + * - `onShow` должен нарисовать экран. + * - `handle` обрабатывает ввод. + * - `inputWindow` говорит движку, из какого окна читать ввод. + * - `close` освобождает ресурс. + */ +interface IScreen +{ + // Вызывается движком, когда экран становится активным (оказался наверху стека) + // или когда движок явно инициирует перерисовку (зависит от реализации). + ScreenAction onShow(ScreenContext context); + // Вызывается движком после закрытия дочернего экрана (Pop/PopTo), + // чтобы передать родителю результат дочернего экрана. + ScreenAction onChildResult(ScreenContext context, ScreenResult child); + // Обработка события ввода. + // Вызывается движком для активного экрана при получении события клавиатуры. + ScreenAction handle(ScreenContext context, KeyEvent event); + + // Окно, из которого движок должен читать ввод для этого экрана. + NCWin inputWindow(); + + // Освобождение ресурсов экрана. + void close(); +} + +abstract class ScreenBase : IScreen +{ +protected: + Window _window; + +public: + override NCWin inputWindow() + { + return _window.handle(); + } + + override ScreenAction onShow(ScreenContext context) + { + return ScreenAction.none(); + } + + override ScreenAction onChildResult(ScreenContext context, ScreenResult child) + { + return ScreenAction.none(); + } + + override ScreenAction handle(ScreenContext context, KeyEvent event) + { + return ScreenAction.none(); + } + + override void close() + { + if (_window !is null) + _window.close(); + _window = null; + } +}