UI-билдер это конструктор, из которого собраны меню и HUD-элементы. Ты не считаешь координаты: просто говоришь, что вот строка, а в ней две кнопки. Движок сам всё раскладывает, анимирует появление, рисует скролл и подсвечивает наведение. Один и тот же ui приходит и в @screen, и в el.layout(fn) у HUD-элемента.
Виджет это один кусочек интерфейса: текст, кнопка, переключатель. Контейнер это коробка, в которой лежат другие виджеты. Всё, что ты создаёшь через ui, встаёт в общее дерево: контейнеры ui.column и ui.row открываются через with, и всё, что вызвано внутри блока, становится их содержимым. Как вложено в коде, так и будет на экране.
@screen("Дерево", 260, 160)
def menu(ui):
with ui.column(gap=8, pad=12, bg=Color(20, 20, 24), fill=True): # корень
ui.text("Заголовок", size=10, weight="bold") # внутри колонки
with ui.row(gap=6, align="center"): # строка внутри колонки
ui.icon("logo", size=12)
ui.text("а это уже внутри строки", size=8)Дерево строится редко: меню когда его открыли, HUD когда сменилась подпись-сигнатура. Поэтому внутри строителя можно спокойно делать циклы, ветвления, собирать списки модулей. А вот значения, которые меняются каждую секунду, передавай функциями, про это раздел про реактивность.
Контейнера всего два. ui.column(**style) ставит детей друг под другом, ui.row(**style) друг за другом. Всё остальное решают стили: gap (расстояние между детьми), pad (отступ от краёв внутрь), align (прижать поперёк) и justify (разложить вдоль).
with ui.row(fill_width=True, align="center", justify="between", pad=(6, 10),
radius=8, bg=Color(30, 31, 36)):
ui.text("Слева", size=8)
ui.text("Справа", size=8)
# распорка: пустая колонка, которая съедает всё свободное место
with ui.row(fill_width=True, align="center", gap=6):
ui.text("Слева", size=8)
ui.column(fill_width=True) # растягивается и толкает соседа вправо
ui.text("Справа", size=8)| Что нужно | Как |
|---|---|
| Растянуть контейнер на всё окно | fill=True |
| Растянуть только по ширине или высоте | fill_width=True / fill_height=True |
| Прижать элемент вправо или вниз | justify="end" либо пустая распорка fill_width |
| Разнести детей по краям | justify="between" |
| Поставить по центру | align="center", justify="center" |
| Прокрутка длинного списка | scroll=True, scrollbar="auto" |
| Слои друг поверх друга | stack=True |
| Перенос на новую строку | wrap=True |
| Разделительная линия | ui.divider() или ui.column(height=1, bg=LINE) |
Вот все листья дерева. Любой из них дополнительно принимает стили из таблицы ниже.
| Виджет | Сигнатура | Зачем |
|---|---|---|
ui.text | (value_or_callable, size=0, weight="medium", color=None, **style) | Текст. Передашь функцию, и текст будет обновляться сам, каждый кадр |
ui.button | (label, **style) | Кнопка клиента; что делать по нажатию, задаёт on_click= |
ui.toggle | (label, get, set, **style) | Строка с подписью и переключателем. set получает новое значение: lambda v: ... |
ui.switch | (get, set, **style) | Переключатель без подписи. set тоже получает новое значение |
ui.slider | (label, lo, hi, value=0.0, on_change=None, step=0.0, **style) | Слайдер с подписью и своим значением внутри |
ui.slider_bar | (get, set, lo, hi, step=0.0, **style) | Голая полоса слайдера; значение живёт в твоих get и set. Записывает вызовом set(v) |
ui.toggle_c | (get, on_click, on=None, off=None, knob=None, **style) | Переключатель со своими цветами. on_click вызывается БЕЗ аргументов |
ui.slider_c | (get, set, lo, hi, step=0, track=, fill=, ring=, inner=, thumb_radius=, thumb_border=, track_height=, **style) | Слайдер со своими цветами и размерами. Записывает вызовом set(v) |
ui.text_input | (get, set, placeholder="", bg=None, color=None, size=0, weight="regular", **style) | Поле ввода. Записывает вызовом set(text). size задаёт размер букв, weight - начертание; поле само становится выше |
ui.swatch | (get, **style) | Кружок цвета, для настроек-цветов |
ui.setting | (py_setting, **style) | Настройка, нарисованная самим клиентом. Запасной вариант для редких типов |
ui.icon | (name, size=10, color=None, **style) | Иконка клиента по имени |
ui.image | (texture, size=10, color=None, radius=0, **style) | Картинка из assets.image() |
ui.space | (px=4) | Пустой промежуток; стили не принимает |
ui.divider | (**style) | Разделительная линия |
ui.color | (name) | Не виджет, а цвет текущей темы клиента |
toggle_c и slider_c это те же переключатель и слайдер, только полностью перекрашиваемые. Именно на них делают свои темы меню, чтобы контролы выглядели не как у клиента, а как ты задумал.
GREEN = Color(52, 199, 89)
OFF = Color(206, 206, 212)
WHITE = Color(255, 255, 255)
TRACK = Color(214, 214, 220)
FILL = Color(60, 60, 66)
mod = Module("Demo", "Visuals")
flag = Checkbox(mod, "Flag")
speed = Slider(mod, "Speed").min(0).max(10).step(0.5).set(5)
@screen("Виджеты", 240, 150)
def menu(ui):
with ui.column(gap=10, pad=12, bg=Color(247, 247, 250), fill=True):
# свой переключатель: get отдаёт состояние, вторая функция срабатывает по клику
with ui.row(fill_width=True, align="center", justify="between", height=24):
ui.text("Flag", size=8, color=Color(30, 30, 34))
ui.toggle_c(lambda: flag.get(), lambda: flag.toggle(),
on=GREEN, off=OFF, knob=WHITE)
# свой слайдер: get читает значение, set(v) записывает
with ui.column(gap=4, fill_width=True):
ui.text(lambda: "Speed: %.1f" % speed.get(), size=8, color=Color(30, 30, 34))
ui.slider_c(lambda: speed.get(), lambda v: speed.set(v),
speed.getMin(), speed.getMax(), speed.getStep(),
track=TRACK, fill=FILL, ring=FILL, inner=WHITE, fill_width=True)Стили это именованные аргументы, которые принимает любой узел: и контейнер, и виджет. Ниже весь список. Всё, чего в нём нет, движок молча проигнорирует.
Размер
| Стиль | Значение | Что делает |
|---|---|---|
width / height | число | Жёстко заданный размер |
size | (w, h) | Ширина и высота одним махом |
min_size / max_size | (w, h) | Границы: меньше или больше не станет |
fill | True | Занять всё свободное место по обеим осям |
fill_width / fill_height | True | То же, но только по одной оси |
Отступы и форма
| Стиль | Значение | Что делает |
|---|---|---|
gap | число | Расстояние между детьми контейнера |
pad / padding | число или (v, h) | Отступ от края внутрь: одинаковый или парой (сверху-снизу, слева-справа) |
radius | число | Скругление углов |
radius_bottom | число | Скругление только нижних углов |
squircle | число | Насколько угол мягкий, как у иконок на телефоне |
color / bg | Color или функция | Заливка фона; функция значит, что цвет пересчитывается каждый кадр |
border | (толщина, Color) | Рамка вокруг узла |
blur | число или (число, Color) | Размыть то, что видно позади узла. Любое число больше нуля включает размытие, силу подбирает сам клиент; в своих панелях он ставит 45. Парой задаётся ещё и подкраска |
glass | True или число | Готовое стекло клиента: размытие плюс тон текущей темы. Числом задаётся насколько оно заметно, от 0 до 1 |
@screen("Glass", 240, 140)
def menu(ui):
# за окном видно мир, поверх ложится своя полупрозрачная заливка
with ui.column(fill=True, gap=8, pad=12, radius=12, blur=45,
bg=Color(16, 17, 20, 150)):
ui.text("Стекло", size=9, color=ui.color("text"))
# готовое стекло клиента: размытие и тон темы одной строкой
with ui.row(fill_width=True, pad=(6, 10), radius=8, glass=True):
ui.text("тон текущей темы", size=7, color=ui.color("text"))Раскладка
| Стиль | Значение | Что делает |
|---|---|---|
align | start | center | end | stretch | Как прижаты дети поперёк оси |
justify | start | center | end | between | around | evenly | Как раскиданы дети вдоль оси |
direction | up | down | left | right | В какую сторону растёт контейнер |
columns | число | Разложить детей в N колонок |
wrap | True | Не влезло, переносим на следующую строку |
center | True | Поставить узел по центру экрана |
collapse | True | Спрятанный узел не занимает места, соседи сдвигаются |
sticky | True | Узел прилипает к краю при прокрутке |
stack | True | Дети рисуются слоями друг поверх друга |
scroll | True | Содержимое можно прокручивать |
scrollbar | auto | always | never | Показывать ли полосу прокрутки |
Мышь
| Стиль | Значение | Что делает |
|---|---|---|
on_click | функция() | Клик по узлу. Работает на контейнерах, кнопке, иконке |
on_click_pos | функция(x, y, button) | То же, но с координатами клика и кнопкой мыши строкой |
cursor | hand | text | crosshair | hresize | vresize | block | resize | Каким станет курсор при наведении |
interactive | bool | Ловит ли узел мышь вообще |
draggable | True или x | y | none | all | Узел можно таскать мышью |
visible_when | функция, отдающая bool | Показывать узел, только пока условие истинно |
fade | True | Плавно гасить края, чтобы длинный текст не обрывался ножом |
Анимации
| Стиль | Значение | Что делает |
|---|---|---|
enter / exit | none | fade | vanish | fade_slide | up | down | left | right | pop | fade_pop | Как узел появляется и как исчезает |
enter_slide / exit_slide | число | Чистый подъезд на N пикселей. Ставится вместо enter и exit, а не вместе с ними: последний вызов перебивает предыдущий |
motion | fast | smooth | signal | spring | spring_snap | soft | Характер движения: от резкого до пружинного |
stagger | число | Задержка между появлением соседних детей, эффект каскада |
Реактивность это когда виджет обновляется сам, без твоего участия. Правило простое: дерево строится редко, значения читаются каждый кадр. Передал вместо готового значения функцию, и движок будет звать её сам, а виджет оживёт. Пересобирать дерево ради этого не надо.
| Куда можно дать функцию | Что оживает |
|---|---|
ui.text(fn, ...) | Сам текст |
bg=fn / color=fn | Цвет фона узла: подсветка активной вкладки, реакция на наведение |
visible_when=fn | Показать или спрятать узел |
get / set у toggle, switch, slider_bar, toggle_c, slider_c, text_input, swatch | Значение контрола |
ACCENT = Color(86, 124, 252)
NONE = Color(0, 0, 0, 0)
state = {"tab": "combat"} # состояние меню держим в обычном dict
@screen("Вкладки", 300, 180)
def menu(ui):
def set_tab(name):
return lambda: state.__setitem__("tab", name)
with ui.column(gap=8, pad=10, bg=Color(12, 13, 15), fill=True):
with ui.row(gap=4):
for name in ("combat", "visuals"):
# клик на строке, а не на тексте: текст мышь не ловит
# bg это функция, поэтому активная вкладка подсвечивается сама
with ui.row(pad=(4, 8), radius=6, cursor="hand", on_click=set_tab(name),
bg=lambda name=name: ACCENT if state["tab"] == name else NONE):
ui.text(name.capitalize(), size=8, color=Color(255, 255, 255))
# visible_when переключает страницы без пересборки дерева
with ui.column(gap=6, fill=True, visible_when=lambda: state["tab"] == "combat"):
ui.text("Combat", size=9, weight="bold", color=Color(255, 255, 255))
with ui.column(gap=6, fill=True, visible_when=lambda: state["tab"] == "visuals"):
ui.text("Visuals", size=9, weight="bold", color=Color(255, 255, 255))Функции спасают, пока меняются только значения. Если меняется сама структура (появился новый модуль, поменялся порядок секций), дерево надо пересобрать: меню сделает это при следующем открытии, а HUD через el.signature(fn).
ui.color(name) отдаёт цвет из текущей темы клиента. Возьми их, и твоё меню будет перекрашиваться вместе с темой пользователя, а не торчать белым пятном.
| Имя | Что это |
|---|---|
accent | Акцентный цвет клиента |
text | Основной цвет текста |
background (или bg) | Фон окна |
second | Второй фон: карточки, контролы |
outline | Рамки и разделители |
white | Чистый белый |
Соберём всё вместе: маленькое меню на цветах темы, которое рисует настоящие модули клиента и их настройки своими виджетами. Настройки достаём так: mod.settings() отдаёт список, у каждой настройки есть type() и свои методы чтения и записи.
CATS = ["COMBAT", "MOVEMENT", "VISUALS", "PLAYER", "OTHER"]
state = {"cat": "COMBAT"}
NONE = Color(0, 0, 0, 0)
@screen("My Menu", 420, 260)
def menu(ui):
accent = ui.color("accent")
text = ui.color("text")
bg = ui.color("background")
second = ui.color("second")
white = ui.color("white")
mods = client.modules.all()
present = [c for c in CATS if any(m.getCategory() == c for m in mods)]
if state["cat"] not in present and present:
state["cat"] = present[0]
def set_cat(c):
return lambda: state.__setitem__("cat", c)
def setting_row(ps):
t = ps.type()
name = ps.name()
if t == "boolean":
with ui.row(fill_width=True, align="center", justify="between", height=16):
ui.text(name, size=7, color=text)
# toggle_c: вторая функция это действие по клику, без аргументов
ui.toggle_c(lambda ps=ps: ps.boolGet(), lambda ps=ps: ps.boolToggle(),
on=accent, off=second, knob=white)
elif t == "slider":
with ui.column(fill_width=True, gap=3):
ui.text(lambda ps=ps: "%s: %.1f" % (ps.name(), ps.numGet()), size=7, color=text)
# slider_c: вторая функция записывает значение, принимает его аргументом
ui.slider_c(lambda ps=ps: ps.numGet(), lambda v, ps=ps: ps.numSet(v),
ps.numMin(), ps.numMax(), ps.numStep(),
track=second, fill=accent, fill_width=True)
else:
ui.setting(ps) # редкий тип, пусть клиент нарисует сам
def card(mod):
with ui.column(fill_width=True, gap=5, pad=8, radius=8, bg=second):
with ui.row(fill_width=True, align="center", justify="between", height=16):
ui.text(mod.getName(), size=8, weight="semibold", color=text)
ui.toggle_c(lambda mod=mod: mod.isEnabled(), lambda mod=mod: mod.toggle(),
on=accent, off=bg, knob=white)
for ps in list(mod.settings()):
if ps.visible():
setting_row(ps)
with ui.column(fill=True, bg=bg, gap=0):
# вкладки категорий: клик вешаем на строку, а не на текст
with ui.row(fill_width=True, align="center", gap=4, pad=(8, 10)):
for c in present:
with ui.row(pad=(4, 8), radius=6, cursor="hand", on_click=set_cat(c),
bg=lambda c=c: accent if state["cat"] == c else NONE):
ui.text(c.capitalize(), size=8, weight="bold", color=text)
ui.divider()
# модули выбранной категории, с прокруткой
for c in present:
with ui.column(fill=True, gap=8, pad=10, scroll=True, scrollbar="auto",
enter="fade_slide",
visible_when=lambda c=c: state["cat"] == c):
for mod in [m for m in mods if m.getCategory() == c]:
card(mod)
m = Module("My Menu", "Other")
Button(m, "Открыть").action(lambda: menu.open())toggle_c и slider_c закрывают галочку и ползунок, а вот у Mode, Select и диапазона перекрашиваемой версии нет вовсе. Значит, если контрол должен выглядеть совсем иначе, его собирают из того же, из чего собрано всё меню: строка, текст, иконка и on_click. Никакого отдельного механизма для этого не нужно.
Настройка при этом остаётся настоящей. Ты не делаешь ей копию, ты рисуешь ей второй вид: значение по-прежнему живёт в настройке, клиент сохраняет его в конфиг, а в обычном меню настройка выглядит как выглядела.
Читают и пишут значение через mod.settings(): список работает не только для чужих модулей, но и для своего. Какие методы у какого типа, собрано в таблице на странице Модули и настройки клиента.
mod = Module("My Menu", "Other")
sort = Mode(mod, "Сортировка").add("Ближний").add("Здоровье")
targets = Select(mod, "Цели").add("Игроки").add("Мобы").add("Животные").select("Игроки").min(1)
ACCENT, BOX, TEXT = Color(86, 124, 252), Color(40, 42, 50), Color(235, 235, 240)
NONE = Color(0, 0, 0, 0)
def ps_of(raw):
# ищем по сырому имени: name() локализовано и на другом языке не совпадёт
for ps in list(mod.settings()):
if ps.rawName() == raw:
return ps
@screen("My Menu", 300, 200)
def menu(ui):
md, sel = ps_of("Сортировка"), ps_of("Цели")
modes, values = list(md.optionLabels()), list(sel.optionLabels())
with ui.column(gap=10, pad=12, radius=12, bg=Color(16, 17, 20), fill=True):
# свой Mode: сегменты вместо клиентских стрелок
with ui.row(gap=2, fill_width=True, pad=2, radius=8, bg=Color(30, 31, 36)):
for i, name in enumerate(modes):
with ui.row(fill_width=True, justify="center", pad=(5, 8), radius=6,
cursor="hand", on_click=lambda i=i: md.modeSelect(i),
bg=lambda i=i: ACCENT if md.modeIndex() == i else NONE):
ui.text(name, size=7, color=TEXT)
# свой Select: квадратные галочки вместо клиентского списка
for i, name in enumerate(values):
with ui.row(fill_width=True, gap=8, align="center", height=20,
cursor="hand", on_click=lambda i=i: sel.selToggle(i)):
with ui.row(size=(14, 14), radius=4, align="center", justify="center",
bg=lambda i=i: ACCENT if sel.selOn(i) else BOX):
ui.icon("check", size=8, color=TEXT,
visible_when=lambda i=i: sel.selOn(i))
ui.text(name, size=7, color=TEXT)
Button(mod, "Открыть").action(lambda: menu.open())Дальше по тому же принципу собирается что угодно: квадратная галочка с иконкой, радиокнопки, выпадающий список, кнопки минус и плюс вместо ползунка. Клик вешают на ui.row, состояние читают функцией в bg= или visible_when, а lambda i=i: в цикле пишут обязательно, иначе все строки будут дёргать последний вариант.