Доблен эксперементальный новый модуль - виджет TextInput.

This commit is contained in:
Alexander Zhirov 2026-01-11 22:51:25 +03:00
parent 2a8362d098
commit 79032d696e
Signed by: alexander
GPG key ID: C8D8BE544A27C511
7 changed files with 280 additions and 11 deletions

View file

@ -7,5 +7,8 @@
"ncui": { "ncui": {
"path": ".." "path": ".."
} }
} },
"libs": [
"form"
]
} }

View file

@ -22,7 +22,12 @@ final class Simple : ScreenBase
override void build(ScreenContext context, Window window, WidgetContainer ui) override void build(ScreenContext context, Window window, WidgetContainer ui)
{ {
auto okBtn = new Button(3, 2, "OK", () => ScreenAction.push(new Simple())); auto textIntput = new TextInput(5, 2, 10, "Hello");
// auto okBtn = new Button(3, 2, "OK", () => ScreenAction.push(new Simple()));
auto okBtn = new Button(3, 2, "OK", () {
textIntput.close();
return ScreenAction.quit(ScreenResult.none());
});
auto cancelBtn = new Button(3, 9, "Cancel", () => ScreenAction.pop(ScreenResult.none())); auto cancelBtn = new Button(3, 9, "Cancel", () => ScreenAction.pop(ScreenResult.none()));
auto disableOk = new Checkbox(4, 2,"Disable OK", false, (checked) { auto disableOk = new Checkbox(4, 2,"Disable OK", false, (checked) {
@ -32,6 +37,7 @@ final class Simple : ScreenBase
_ui.add(okBtn); _ui.add(okBtn);
_ui.add(cancelBtn); _ui.add(cancelBtn);
_ui.add(disableOk); _ui.add(disableOk);
_ui.add(textIntput);
} }
override ScreenAction handleGlobal(ScreenContext context, KeyEvent event) override ScreenAction handleGlobal(ScreenContext context, KeyEvent event)

View file

@ -22,4 +22,14 @@ struct NCWin
{ {
return _p is null; return _p is null;
} }
void opAssign(NCWin rhs)
{
_p = rhs._p;
}
void opAssign(WINDOW* rhs)
{
_p = rhs;
}
} }

View file

@ -131,12 +131,13 @@ final class Session
private: private:
NCWin _root; NCWin _root;
bool _ended; bool _ended;
SessionConfig _config;
// Применяет параметры конфигурации к активной ncurses-сессии. // Применяет параметры конфигурации к активной ncurses-сессии.
void setup(ref const(SessionConfig) config) void setup(ref const(SessionConfig) sc)
{ {
// Настройка режима обработки ввода терминалом. // Настройка режима обработки ввода терминалом.
final switch (config.mode) final switch (sc.mode)
{ {
case InputMode.raw: case InputMode.raw:
ncuiNotErr!nocbreak(); ncuiNotErr!nocbreak();
@ -154,7 +155,7 @@ private:
break; break;
} }
// Настройка отображения вводимых символов. // Настройка отображения вводимых символов.
final switch (config.echo) final switch (sc.echo)
{ {
case Echo.on: case Echo.on:
ncuiNotErr!echo(); ncuiNotErr!echo();
@ -165,15 +166,15 @@ private:
break; break;
} }
// Настройка видимости курсора. // Настройка видимости курсора.
ncuiNotErr!curs_set(config.cursor); ncuiNotErr!curs_set(sc.cursor);
// Настройка задержки при нажатии на ESC // Настройка задержки при нажатии на ESC
ncuiNotErr!set_escdelay(config.escDelay); ncuiNotErr!set_escdelay(sc.escDelay);
// Настройка обработки специальных клавиш // Настройка обработки специальных клавиш
ncuiNotErr!keypad(_root, config.keypad); ncuiNotErr!keypad(_root, sc.keypad);
} }
public: public:
this(const SessionConfig config) this(const SessionConfig sc)
{ {
// Если на этапе инициализации сработает проблема с конфигурированием сессии // Если на этапе инициализации сработает проблема с конфигурированием сессии
scope (failure) scope (failure)
@ -193,7 +194,9 @@ public:
// Установить флаг инициализации ncurses // Установить флаг инициализации ncurses
gInitialized = true; gInitialized = true;
// Применение конфигурации // Применение конфигурации
setup(config); setup(sc);
_config = sc;
} }
NCWin root() NCWin root()
@ -201,6 +204,11 @@ public:
return _root; return _root;
} }
@property SessionConfig config()
{
return _config;
}
KeyEvent readKey(NCWin inputWindow) KeyEvent readKey(NCWin inputWindow)
{ {
dchar ch; dchar ch;

View file

@ -14,6 +14,8 @@ protected:
WidgetContainer _ui; WidgetContainer _ui;
bool _built; bool _built;
bool _clearNext = true;
void ensureWindow(ScreenContext context); void ensureWindow(ScreenContext context);
void build(ScreenContext context, Window window, WidgetContainer ui); void build(ScreenContext context, Window window, WidgetContainer ui);
void layout(ScreenContext context, Window window, WidgetContainer ui) void layout(ScreenContext context, Window window, WidgetContainer ui)
@ -31,7 +33,12 @@ private:
import deimos.ncurses : doupdate; import deimos.ncurses : doupdate;
import ncui.lib.checks; import ncui.lib.checks;
_window.erase(); if (_clearNext)
{
_window.erase();
_clearNext = false;
}
layout(context, _window, _ui); layout(context, _window, _ui);
_ui.render(_window, context); _ui.render(_window, context);
_window.noutrefresh(); _window.noutrefresh();
@ -65,6 +72,8 @@ public:
_built = true; _built = true;
} }
_clearNext = true;
renderAll(context); renderAll(context);
return ScreenAction.none(); return ScreenAction.none();
} }
@ -103,6 +112,7 @@ public:
_window = null; _window = null;
_built = false; _built = false;
_clearNext = true;
_ui = new WidgetContainer(); _ui = new WidgetContainer();
} }
} }

View file

@ -19,3 +19,4 @@ public import ncui.lib.logger;
public import ncui.widgets.container; public import ncui.widgets.container;
public import ncui.widgets.button; public import ncui.widgets.button;
public import ncui.widgets.checkbox; public import ncui.widgets.checkbox;
public import ncui.widgets.textinput;

View file

@ -0,0 +1,231 @@
module ncui.widgets.textinput;
import deimos.form;
import ncui.widgets.widget;
import ncui.core.window;
import ncui.core.event;
import ncui.core.ncwin;
import ncui.engine.screen;
import ncui.engine.action;
import ncui.lib.checks;
import std.string : toStringz;
alias OnChange = void delegate(string text);
final class TextInput : IWidget
{
private:
OnChange _onChange;
int _y;
int _x;
int _width;
bool _enabled = true;
FIELD* _field;
// 0: field, 1: null (терминатор для new_form)
FIELD*[2] _fields;
FORM* _form;
NCWin _window;
NCWin _subWindow;
bool _posted = false;
void ensureCreated()
{
if (_form !is null)
{
return;
}
_field = new_field(1, _width, 0, 0, 0, 0);
set_field_back(_field, A_UNDERLINE);
field_opts_off(_field, O_AUTOSKIP);
field_opts_on(_field, O_EDIT);
_fields[0] = _field;
_fields[1] = null;
_form = new_form(cast(FIELD**) _fields.ptr);
}
void bindTo(Window window)
{
auto parent = window.handle;
if (_window.ptr == parent.ptr && !_subWindow.isNull)
{
return;
}
if (_posted)
{
unpost_form(_form);
_posted = false;
}
if (!_subWindow.isNull)
{
delwin(_subWindow);
_subWindow = NCWin(null);
}
_subWindow = derwin(parent, 1, _width, _y, _x);
_window = parent;
set_form_win(_form, parent);
set_form_sub(_form, _subWindow);
post_form(_form);
_posted = true;
}
void applyStyle(ScreenContext context, bool focused)
{
if (focused && _enabled)
{
set_field_back(_field, A_UNDERLINE | A_REVERSE);
pos_form_cursor(_form);
import ncui.core.session : Cursor;
curs_set(Cursor.high);
}
else
{
set_field_back(_field, A_UNDERLINE);
curs_set(context.session.config.cursor);
}
}
void driveRequest(int request)
{
form_driver_w(_form, KEY_CODE_YES, request);
}
void driveChar(dchar ch)
{
form_driver_w(_form, OK, ch);
}
public:
this(int y, int x, int width, string initial = string.init, OnChange onChange = null)
{
_y = y;
_x = x;
_width = width;
_onChange = onChange;
ensureCreated();
if (initial.length)
{
set_field_buffer(_field, 0, initial.toStringz);
}
}
override @property bool focusable()
{
return true;
}
override @property bool enabled()
{
return _enabled;
}
override void render(Window window, ScreenContext context, bool focused)
{
ensureCreated();
bindTo(window);
applyStyle(context, focused);
}
override ScreenAction handle(ScreenContext context, KeyEvent event)
{
if (!_enabled)
{
return ScreenAction.none();
}
bool changed = false;
if (event.isKeyCode)
{
switch (event.ch)
{
case KEY_LEFT:
driveRequest(REQ_PREV_CHAR);
break;
case KEY_RIGHT:
driveRequest(REQ_NEXT_CHAR);
break;
case KEY_HOME:
driveRequest(REQ_BEG_LINE);
break;
case KEY_END:
driveRequest(REQ_END_LINE);
break;
case KEY_BACKSPACE:
driveRequest(REQ_DEL_PREV);
changed = true;
break;
case KEY_DC:
driveRequest(REQ_DEL_CHAR);
changed = true;
break;
default:
return ScreenAction.none();
}
}
else if (event.isChar)
{
// Backspace иногда приходит как символ (127 или '\b')
if (event.ch == 127 || event.ch == '\b')
{
driveRequest(REQ_DEL_PREV);
changed = true;
}
else
{
driveChar(event.ch);
changed = true;
}
}
return ScreenAction.none();
}
void close()
{
if (_posted && _form !is null)
{
unpost_form(_form);
_posted = false;
}
if (_form !is null)
{
free_form(_form);
_form = null;
}
if (_field !is null)
{
free_field(_field);
_field = null;
}
if (!_subWindow.isNull)
{
delwin(_subWindow);
_subWindow = NCWin(null);
}
_window = NCWin(null);
}
~this()
{
close();
}
}