Skip to content
tphp-langPublic

About

🐟绑定cui 的TingyPHP.rt ext库

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tphp/tui

基于 CUI 1.4.0 的 tphp 窗口 UI 扩展包(type=ext), 单类 Tphp\Tui\App 立即模式封装,310 个方法,覆盖 CUI 组件面(覆盖矩阵与排除清单见 doc/coverage.md)。

链入

build\tphp.exe ext --add C:\project\tphp\tui     # 编译 + 重链(换像,下次运行生效)
build\tphp.exe run example/hello.php             # 交互示例

POSIX 上为 ./build/tphp ext --add …。窗口后端按平台分栏(mod.json os.<平台>.sources):

平台 后端 额外依赖
Windows cui/backends/win32 无(引擎默认库 + 包内 gdi32/opengl32/imm32/dwmapi/comdlg32)
Linux cui/backends/x11 X11 开发头文件(libx11-dev / xorg-x11-devel),链接 -lX11
macOS cui/backends/cocoa(.m,-fobjc-arc) 系统 SDK 自带 Cocoa/QuartzCore/OpenGL frameworks

主循环与语义约定

$app = new \Tphp\Tui\App("demo", 1080, 720);   // 第4参 true = hidden(测试用)
while ($app->frame()) {                        // 渲染上一段 + 等事件 + 开新段;关窗 false
    $app->heading("Hello");
    if ($app->button("点我")) { /* clicked */ }
    $on = $app->toggle("开关", $on);           // 返回新值(PHP 持状态)
}
$app->destroy();
  • 返回三态:bool = 点击/可见/翻转结果;int = 新索引/句柄/页码;关联数组 = 结构化结果 (如 pathBar() 返回 ['clicked','submitted','editing'])。值型控件一律返回新值。
  • buffer 往返:input/textarea/search/otpInput/tagInput/codeEditor/dataGridCellEdit 值由 PHP 持有,传入返回出(idle 帧原样返回)。
  • scope 成对:xxxBegin/xxxEnd 必须配对;open 类参数传入当前值、返回新值。
  • 单例/静态暂存:terminal/log 需先 terminalInit()/logInit();hex 选择、image 视图、 裁剪矩形、split ratio、table/dataGrid/treeTable 的 columns 等跨段暂存(同类 scope 每段至多一层)。
  • 错误一律抛异常(TypeError/ValueError/LogicException),无静默。

PHP 侧使用指南

最小应用

<?php
use Tphp\Tui\App;

$app = new App("我的窗口", 800, 600);      // 第4参 true = 隐藏窗口(测试)
while ($app->frame()) {                    // 渲染上一段 → 等事件 → 开新段;关窗返回 false
    $app->heading("Hello tui");
    if ($app->button("退出")) {
        $app->quit();                      // 下一次 frame() 返回 false,循环自然结束
    }
}
$app->destroy();                           // 释放窗口/纹理(循环退出后必调)

组件调用只发生在 frame() 返回 true 的段内(立即模式:每帧重放一遍调用即可, 不需要手工维护控件树)。frame() 会阻塞等事件——不要在段外空转调用组件。

状态管理四范式

立即模式没有控件对象,状态全部放在 PHP 变量里,每帧传入、取回新值:

// 1) 值型:返回新值(checkbox/toggle/slider/select/tabs/...)
$agree = false;
$tab   = 0;
$vol   = 0.5;
while ($app->frame()) {
    $agree = $app->checkbox("同意协议", $agree);
    $tab   = $app->tabs("main", ["首页", "设置"], $tab);   // 点击后返回新索引
    $vol   = $app->slider("音量", $vol, 0.0, 1.0);
}

// 2) buffer 往返:值由 PHP 持有,传入返回出(input/textarea/search/otp/tag/codeEditor)
$text = "";
while ($app->frame()) {
    $text = $app->input("q", $text, 256, ["label" => "搜索"]);
    if ($app->button("提交") && trim($text) !== "") {
        do_search($text);
        $text = "";
    }
}

// 3) open 类:传入当前展开态,返回本帧新态(dialog/accordion/tree/banner/...)
$dialog_open = false;
while ($app->frame()) {
    if ($app->button("打开对话框")) $dialog_open = true;
    if ($app->dialogBegin("标题", $dialog_open)) {
        $app->text("内容");
        if ($app->button("关闭")) $dialog_open = false;
        $app->dialogFooterBegin();
        $app->button("OK");
        $app->dialogFooterEnd();
        $app->dialogEnd();                  // Begin/End 必须配对(关态时封装层自动跳过)
    }
}

// 4) scope 成对:xxxBegin / xxxEnd 每帧一一配对(table/scroll/split/menu/...)
while ($app->frame()) {
    if ($app->tableBegin("t1", [["title" => "名"], ["title" => "值"]])) {
        $app->tableRow(false);
        $app->tableCell();  $app->tableCellText("a");
        $app->tableCell();  $app->tableCellText("1");
        $app->tableEnd();
    }
}

常见场景片段

// 计数器(点击 +1,按钮返回 bool)
$count = 0;
$app->frame();
if ($app->button("+1")) $count++;
$app->text("count = " . $count);

// 列表增删(数组即状态)
$items = ["a", "b"];
$keep = [];
foreach ($items as $it) {
    $on = $app->toggle($it, false);            // 行内勾选态也可放平行数组
    if (!$app->button("删")) $keep[] = $it;
}
$items = $keep;

// 轻反馈:toast 句柄 + alert/banner(返回 open/可见态)
$th = $app->toast("已保存");
if ($app->toastActionClicked($th)) { /* ... */ }

// 图表数据 = 每帧构造的 PHP 数组
$app->lineChart("c1", [
    ["name" => "访问", "values" => [120.0, 180.0, 150.0], "color" => 0x3B82F6FF],
], ["height" => 180]);

// 日期:[y, m, d] 三元组往返
$d = $app->datePicker("d1", [2026, 11, 15]);
$app->muted("选中 {$d[0]}-{$d[1]}-{$d[2]}");

异常处理

所有参数错误都抛异常,可正常 try/catch(tphp 原生异常):

try {
    $app->setAccent(99);                     // ValueError: accent must be in 0..7
} catch (ValueError $e) {
    echo $e->getMessage(), "\n";
}

TypeError(类型不符)/ ValueError(取值越界)/ LogicException(时序错误,如未 terminalInit() 就 terminal())三类;没有静默失败分支。

写测试(hidden 窗口骨架)

<?php
$fail = 0;
function check(bool $c, string $n): void {
    global $fail;
    if ($c) echo "PASS $n\n"; else { echo "FAIL $n\n"; $fail++; }
}

$app = new \Tphp\Tui\App("t", 640, 480, true);   // 隐藏窗口
$app->frame();                                    // 开首段
// —— 每帧内容:绘制 + 断言返回值 ——
check($app->button("hi") === false, "idle-not-clicked");
$app->quit();
check($app->frame() === false, "loop-stopped");
$app->destroy();
echo $fail ? "SMOKE=FAIL\n" : "SMOKE=OK\n";
exit($fail ? 1 : 0);

运行 tphp run tests\xxx.php;整套冒烟用 tests\run.ps1 一键驱动。需要测 点击/翻转等交互时,用 simulateMouse/simulateMouseButton/simulateKey 注入 + lastRect() 定位(时序规则见 doc/coverage.md §4)。

使用注意

  • xxxBegin() 返回 false(未展开/不可见)时,对应 xxxEnd() 仍要无条件调用 ——封装层已按 SCOPE_IF 语义内部守卫,直接配对写即可。
  • 同一组件每段只调用一次;id 类参数(input("q",...) 等)在同帧内保持稳定。
  • 资源型对象(imageLoad 的纹理)用完调 imageDestroy;窗口结束调 destroy()。
  • terminal()/logView() 等单例组件需先 terminalInit()/logInit(),否则抛 LogicException。
  • 需要真实显示窗口时去掉第4参(hidden),关窗即 frame() 返回 false。

API 分组表(310 方法)

核心(tui.c)

组 方法 返回
生命周期 __construct(title,w,h,hidden=false)、frame()、quit()、destroy() frame: bool
窗口 setTitle(title)、setSize(w,h) void
布局原语 columnBegin(pad,gap)、rowBegin(pad,gap)、boxEnd()、spacer()、separator() void
基础文本 heading(s)、secondary(s)、text(s)、label(s) void
基础控件 button(label)、toggle(label,value) bool(点击 / 新值)

显示小件(tui_widgets.c)

组 方法 返回
文本 muted code kbd tooltip gap(size) void;link bool
图标 icon(name,size=0,color=-1)(0xRRGGBBAA,-1=主题色) void
卡片 cardBegin / cardEnd bool / void
徽章头像 badge avatar avatarGroup(names,maxVisible=3,size=32) void
进度 progress(value) progressRing(label,value,size=48) spinner(size=24) skeleton(height=20) loader() void
状态 emptyState(title) metric(label,value) meter(label,used,total,unit="") bool / void / void
按钮 iconButton copyButton(id,text) holdButton confirmButton statusButton(label,status=0..3) bool
buttonGroupBegin/End、splitButton(label,items[]): int(-1 无) void / int
社交 reactions(id,items[]): int(items=[icon,count,active]) int
文本特效 shimmerText textReveal(id,s) wordRotate(id,words,interval=2) numberRoll(id,s) gradientText counter(id,v) ticker(id,items) void;typewriter(id,s): bool

表单(tui_form.c)

组 方法 返回
buffer 往返 input(id,value,cap=512,opts=[])、textarea(cap=4096)、search(cap=256)、otpInput(id,code,len=6)、tagInput(id,buf,cap=512)、moneyInput(id,value) string / float
数值 number(id,v,min,max) slider(label,v,min,max) rulerSlider knob fader scrub float(新值)
rangeSlider(label,lo,hi,min,max)、histogramSlider(label,bins,lo,hi,min,max) array{lo,hi}
levelMeter(id,levels[]): bool bool
勾选选择 checkbox(label,v): bool、chip(label,v): bool bool(新值)
radio(label,current,option): int、radioGroup(id,items,index): int、segmented(id,items,index): int、select(id,items,index): int、wheelPicker(id,items,index): int、rating(id,v,max): int int(新索引)
toggleGroup(id,labels,values): bool[]、multiSelect(id,items,selected): bool[] bool[](新状态)
日期时间 dateToday(): [y,m,d]、calendar(id,date)、datePicker(id,date) [y,m,d]
dateRangePicker(id,start,end)、rangeCalendar(id,start,end,months=1) [start,end]
timePicker(id,h,m)、timeDial(id,h,m,size=64) → [h,m];colorPicker(id,color): int(0xRRGGBBAA)

容器与导航(tui_layout.c)

组 方法 返回
容器 boxBegin(pad=0,gap=0)/boxEnd、scrollBegin(id="",horizontal=false)/scrollEnd/scrollTo(id,y) void
splitBegin(id,ratio)/splitNext/splitEnd splitEnd: float(新比例)
导航 tabs(id,items,index): int、navItem(label): bool、navSection、breadcrumb(items): int(-1 无)、pagination(id,page,pageCount): int、stepper(steps,current)
pathBar(id,segments): assoc、pathBarMenu(id): int、pathBarMenuEnd、toc(scrollId,items): int、tocAnchor
覆盖层 popoverBegin/End/Open/Close/Toggle/IsOpen、menuBegin/End/Item/Separator/Label、contextMenuBegin/End 见名
dialogBegin(title,open): bool/End/dialogFooterBegin/End、sheetBegin/End、alert(title): bool(dismiss 前 true)、banner(text,open): bool open 语义
toast(title): int 句柄、toastActionClicked(h): bool、toastDismiss(h)、hoverCardBegin/End、userMenuBegin/End
speedDial(id,actions): int(-1 无)、spotlightBegin/End
列表树 accordionBegin(title,open): bool/End、treeBegin(label,open): bool/End/、treeClicked(): bool、listItem(title): bool
virtualList(id,count,rowHeight,height=0): assoc{first,last}/End、swipeItemBegin(id,left,right)/End: int、reorderBegin(id,order)/End: int[]
表格 tableBegin(id,columns): bool/tableRow(selected=false): bool/tableCell/tableCellText/tableEnd columns=[title,sortable]
dataGridBegin(id,columns,rowCount): false|assoc{first,last}/Row(row): bool/Cell/CellText/CellEdit(value,cap=256): string/End columns 增 width/align/fixed/priority
treeCell(label,depth,hasChildren,open,icon=""): bool、treeTableBegin(id,columns,nodes)/Row(): int/Name/Depth/End: assoc、filterBar(id,fields,selected,search): assoc nodes=[parent,first_child,next_sibling,flags]
工具条 toolbarBegin(id)/End/+Button(icon): bool/Separator/Spacer、statusBarBegin/End/Spacer/Text/Item: bool/Progress(v)、menuBarBegin/End/MenuBegin: bool/MenuEnd
停靠 dockBegin/End/Item(icon,label): bool/Separator、dockspaceBegin/End、dockPanelBegin(title,open): bool/End、dockSave(id): string、dockLoad(id,json): bool
轮播看板 carouselBegin(id,count,index): int/SlideBegin/End/End、kanbanBegin/End: assoc|null+ColumnBegin(title,n): bool/CardBegin(id): bool/CardEnd/ColumnEnd、expandableCardBegin(title,open): bool/End、cardStackBegin(id,count)/ItemBegin(depth)/ItemEnd/End: int

图表(tui_charts.c)

组 方法 返回
基础 lineChart(id,series,opts) barChart(id,series,opts)(series=[name,values,color?];opts=[labels,area,no_legend,no_grid,stacked,smooth,format,height,min,max]) void
donutChart(id,values,labels,opts)、sparkline(values,width=100,height=24,color=0)、gauge(label,v,size=120)、radialGauge(id,v,min,max) void
scatterChart(id,series): int(points=[[x,y]])、radarChart(id,series,axes): int int(hover)
网格 heatmap(id,values,rows,cols): int、calendarHeatmap(id,start,values): int int
展示 treemap/funnel/waffle/splitBar/barList/activityRings(id,items): int(items=[label,value,color?]) int
sankey(id,nodes,links): int(links=[from,to,value]) int
金融事件 candlestick(id,candles,labels=[]): int(candles=[open,high,low,close,volume])、waterfall(id,requests): assoc{clicked,activated,hovered}
dotChart(id,series): assoc{hovered,x,value,clicked}、timelineItem(title)

富内容(tui_content.c)

组 方法 返回
代码文本 codeBlock(id,code,lang="",title="")、jsonView(id,json): bool(合法=true)、markdown(id,text): assoc
codeEditor(id,code,lang=""): assoc{text,changed,refused,focused,cursor,anchor,line,column,…} + codeEditorGoto/Select/Find
diffView(id,old,new): assoc{added,removed,changes,current}、hexView(id,bytes): assoc + hexViewGoto(id,offset)
终端日志 terminalInit(textCap=65536,lineCap=4096)/terminalWrite(s)/terminalClear()/terminal(id): assoc{command,interrupt} 单例
logInit(textCap,lineCap)/logAppend(level 0..4,text)/logClear()/logView(id,timestamps=false) 单例
向导计划 commandPalette(id,open,commands): assoc{picked,open}、tour(id,steps,step): assoc{event,step}、gantt(id,tasks,origin,days): int
文件 fileBrowser(id,path): assoc{path,picked,selected,navigated,error}、filePicker(id,open,path=""): assoc{open,path}
图片 imageLoad(path): assoc{tex,w,h}、imageCreate(w,h,rgba): int、imageDestroy(tex)、image(tex)、imageViewer(id,tex,w,h): assoc{zoom,cx,cy}、cropper(id,tex,w,h): assoc{x,y,w,h}
emoji emoji(s,size=0)、emojiPicker(id): assoc{picked}、emojiSupported(s): bool、emojiFontLoadFile(path): bool、emojiFontLoadSystem(): bool
社交评论 chatMessage(text,outgoing=false,author="",markdown=false)、chatDivider(label)、typingIndicator(name)、comment(id,author,text): int、presence(id,users,maxVisible=3,size=32)
签名 signaturePad(id): bool、signaturePoints(id): assoc{points,strokes}、signatureClear(id)
节点编辑 nodeEditorBegin(id): false|assoc{x,y,zoom}/End、nodeBegin(id,x,y,title): assoc{x,y,selected}/End、nodeInput/Output(pin,label,type=0): bool、nodeContentBegin/End、nodeLink(id,from,to): bool、nodeLinkCreated/Removed/Deleted: assoc|null
devtools testExplorer(id,items): assoc、stackTrace(id,title,message,frames,lang=""): assoc{clicked}、requestBar(id,url,method=0,loading=false,placeholder=""): assoc、responseStatus(status,seconds,bytes)
uptime(id,services): assoc{service,day}、timeHistogram(id,start,bucketSeconds,buckets,series,selStart=0,selEnd=0): assoc、coverage(id,files): assoc{activated,lines}、blame(id,lines,lineCommit,commits): assoc{clicked,hovered}
featureFlags(id,flags,envs): assoc{flag,env,flags}、apiExplorer(id,endpoints): assoc{selected,try_it}、readout(label,value)、dotDisplay(text)

App 层与测试面(tui_app.c)

组 方法 返回
截图 savePng(path): bool、screenshotRgba(): assoc{w,h,data}
系统 fileDialog(kind=0,title="",filterName="",filter="",defaultName=""): string|false(冒烟不调用)、openUrl(url): bool(同)
主题查询 setTheme(0 light/1 dark)、setAccent(0..7)、setReducedMotion(bool)、dpiScale(): float、size(): assoc{w,h}、backend(): string、droppedFiles(): string[]
测试面 simulateMouse(x,y)、simulateMouseButton(0 left/1 right/2 middle, down=true)、simulateKey(cui_key枚举值,down,mods=0)、simulateText(utf8)、lastRect(): assoc{x,y,w,h} 见 coverage.md「测试面时序」

测试

powershell -ExecutionPolicy Bypass -File tests\run.ps1     # cwd = tui 包根

7 个冒烟(basic/widgets/form/container/chart/advanced/interaction)全部 hidden 窗口、 自断言、exit code 语义;interaction 用输入注入驱动真实点击(button 返回 true、toggle 翻转、 PNG 截图落盘校验)。任一失败退出码非 0。

示例

文件 内容
example/hello.php 最小循环 + 计数按钮
example/todo.php 输入添加、勾选计数、删除/清空
example/form.php 表单控件全集(文本/数值/选择/日期)
example/dashboard.php 指标 + 折线/柱状/环形/仪表 + 表格
example/markdown.php markdown / codeBlock / jsonView / hexView / diffView

运行:build\tphp.exe run example\<name>.php(POSIX 同理)。

About

🐟绑定cui 的TingyPHP.rt ext库

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages