exeasyui.lua 59 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740
  1. --[[
  2. exEasyUI - 简化的UI组件库
  3. 版本号:1.6.1
  4. 作者: zengshuai
  5. 日期:2025-10-09
  6. =====================================
  7. 结构说明:
  8. 1. 常量定义 - UI颜色常量和调试配置
  9. 2. 硬件依赖 - 使用exlcd/extp初始化LCD和TP,并使用gtfont初始化字体(可选)
  10. 3. 核心部分 - 组件管理、事件分发、渲染系统
  11. 4. 组件部分 - 目前有6个组件
  12. - Button:按钮组件
  13. - CheckBox:复选框组件
  14. - Label:标签组件
  15. - Picture:图片组件
  16. - MessageBox:消息框组件
  17. - Window:窗口组件
  18. - ProgressBar:进度条组件
  19. 基于原exSimpleUI重构,将所有代码合并为单个文件,便于使用和维护。
  20. 支持触摸事件分发、组件渲染、主题切换等核心功能。
  21. ]]
  22. local screen_data = require "screen_data_table" -- 唯一引入屏幕配置参数的地方
  23. local exlcd = require "exlcd" -- 显示驱动模块
  24. local extp = require "extp" -- 触摸驱动模块
  25. gtfont_dev = gtfont_dev or nil -- 全局SPI设备句柄,避免被GC
  26. -- ================================
  27. -- 1. 常量定义
  28. -- ================================
  29. -- UI颜色常量
  30. local COLOR_WHITE = 0xFFFF
  31. local COLOR_BLACK = 0x0000
  32. local COLOR_GRAY = 0x8410
  33. local COLOR_BLUE = 0x001F
  34. local COLOR_RED = 0xF800
  35. local COLOR_GREEN = 0x07E0
  36. local COLOR_YELLOW = 0xFFE0
  37. local COLOR_CYAN = 0x07FF
  38. local COLOR_MAGENTA = 0xF81F
  39. local COLOR_ORANGE = 0xFC00
  40. local COLOR_PINK = 0xF81F
  41. -- Windows 11 风格颜色(v1.6.0新增)
  42. -- Light模式
  43. local COLOR_WIN11_LIGHT_DIALOG_BG = 0xF79E -- RGB(243, 243, 243) - 对话框背景
  44. local COLOR_WIN11_LIGHT_BUTTON_BG = 0xFFDF -- RGB(251, 251, 252) - 按钮背景
  45. local COLOR_WIN11_LIGHT_BUTTON_BORDER = 0xE73C -- RGB(229, 229, 229) - 按钮边框
  46. -- Dark模式
  47. local COLOR_WIN11_DARK_DIALOG_BG = 0x2104 -- RGB(32, 32, 32) - 对话框背景
  48. local COLOR_WIN11_DARK_BUTTON_BG = 0x3186 -- RGB(51, 51, 51) - 按钮背景
  49. local COLOR_WIN11_DARK_BUTTON_BORDER = 0x4A69 -- RGB(76, 76, 76) - 按钮边框
  50. -- ================================
  51. -- 2. 硬件依赖部分 (hw)
  52. -- ================================
  53. local hw = {}
  54. local FontAdapter = { _backend = "default", _size = 12, _gray = false, _name = nil }
  55. -- 硬件初始化入口
  56. function hw.init(opts)
  57. -- 初始化显示屏
  58. -- 使用screen_data配置表中的参数初始化LCD,在配置表中修改即可
  59. local lcd_init_success exlcd.init(screen_data.lcdargs)
  60. -- 检查LCD初始化是否成功
  61. if lcd_init_success then
  62. log.error("ui_main", "LCD初始化失败")
  63. return -- 初始化失败,退出任务
  64. end
  65. -- 通用显示设置
  66. lcd.setupBuff(nil, false) -- 设置帧缓冲区
  67. lcd.autoFlush(false) -- 禁止自动刷新
  68. -- -- 设置字体为模组自带的opposansm12中文字体
  69. -- lcd.setFont(lcd.font_opposansm12_chinese)
  70. -- 初始化触摸IC
  71. -- 使用配置表中的参数初始化触摸
  72. extp.init(screen_data.touch)
  73. extp.setPublishEnabled("all", true) -- 发布所有消息
  74. -- 自定义配置
  75. extp.setSlideThreshold(40) -- 设置滑动阈值为40像素
  76. extp.setLongPressThreshold(600) -- 设置长按阈值为600毫秒
  77. -- 字体后端装配(保持原逻辑,可选)
  78. local fcfg = opts.font or {}
  79. if fcfg.type == "gtfont" then
  80. local spi_id = (fcfg.spi and fcfg.spi.id) or 0
  81. local spi_cs = (fcfg.spi and fcfg.spi.cs) or 8
  82. local spi_clk = (fcfg.spi and fcfg.spi.clock) or (20 * 1000 * 1000)
  83. gtfont_dev = spi.deviceSetup(spi_id or 1, spi_cs or 12, 0, 0, 8, spi_clk or (20*1000*1000), spi.MSB, 1, 0)
  84. log.error("exEasyUI.gtfont", "spi.deviceSetup", type(gtfont_dev))
  85. if type(gtfont_dev) ~= "userdata" then
  86. log.error("exEasyUI.gtfont", "spi.deviceSetup error", type(gtfont_dev))
  87. gtfont_dev = nil
  88. end
  89. local gtfont_ok = gtfont.init(gtfont_dev)
  90. if gtfont_ok then
  91. FontAdapter._backend = "gtfont"
  92. FontAdapter._size = tonumber(fcfg.size or 16)
  93. FontAdapter._gray = not not fcfg.gray
  94. log.info("exEasyUI", "gtfont enabled", spi_id, spi_cs, FontAdapter._size)
  95. else
  96. FontAdapter._backend = "default"
  97. FontAdapter._size = 12
  98. FontAdapter._gray = false
  99. log.warn("exEasyUI", "gtfont init failed, fallback to default font")
  100. end
  101. else
  102. FontAdapter._backend = "default"
  103. FontAdapter._size = 12
  104. FontAdapter._gray = false
  105. FontAdapter._name = (fcfg and fcfg.name) or nil
  106. if lcd and lcd.setFont and lcd.font_opposansm12_chinese then
  107. lcd.setFont(lcd.font_opposansm12_chinese)
  108. end
  109. end
  110. return true
  111. end
  112. -- ================================
  113. -- 3. 核心部分 (core + event)
  114. -- ================================
  115. local core = {}
  116. local event = {}
  117. -- 组件注册表
  118. local registry = {}
  119. local last_action = nil
  120. local current_theme = "dark"
  121. -- 调试开关
  122. core.debug_touch = true
  123. -- 调试配置函数
  124. function core.debug(v)
  125. if v == nil then return { touch = not not core.debug_touch } end
  126. if type(v) == "boolean" then
  127. core.debug_touch = v
  128. return
  129. end
  130. if type(v) == "table" then
  131. if v.touch ~= nil then core.debug_touch = not not v.touch end
  132. return
  133. end
  134. end
  135. -- 添加组件到渲染队列
  136. function core.add(component)
  137. registry[#registry + 1] = component
  138. end
  139. -- 从注册表移除组件
  140. function core.remove(component)
  141. for i = #registry, 1, -1 do
  142. if registry[i] == component then
  143. table.remove(registry, i)
  144. return true
  145. end
  146. end
  147. return false
  148. end
  149. -- 清屏
  150. function core.clear(color)
  151. lcd.clear(color or COLOR_BLACK)
  152. end
  153. -- 渲染所有可见组件
  154. function core.render()
  155. for i = 1, #registry do
  156. local c = registry[i]
  157. if c and c.visible ~= false and c.draw then c:draw() end
  158. end
  159. lcd.flush()
  160. end
  161. -- 获取当前时间戳
  162. local function now_ms()
  163. if mcu and mcu.ticks then return mcu.ticks() end
  164. return (os.clock() or 0) * 1000
  165. end
  166. -- 命中测试
  167. local function hit_test(x, y, r)
  168. return x >= r.x and y >= r.y and x <= (r.x + r.w) and y <= (r.y + r.h)
  169. end
  170. -- 触摸事件分发(extp事件)
  171. function core.handleTouchEvent(evt, x, y)
  172. local start_ms
  173. if core.debug_touch then
  174. start_ms = now_ms()
  175. end
  176. for i = #registry, 1, -1 do
  177. local c = registry[i]
  178. if c and c.enabled ~= false and c.handleEvent and
  179. (c._capture == true or hit_test(x, y, { x = c.x, y = c.y, w = c.w, h = c.h })) then
  180. if c:handleEvent(evt, x, y) then
  181. if core.debug_touch and start_ms then
  182. local dt = now_ms() - start_ms
  183. log.info("exEasyUI", "consumed_by", tostring(c.__name or "component"), string.format("%.2fms", dt))
  184. end
  185. return true
  186. end
  187. end
  188. end
  189. if core.debug_touch and start_ms then
  190. local dt = now_ms() - start_ms
  191. if evt ~= "MOVE_X" and evt ~= "MOVE_Y" then
  192. log.info("exEasyUI", "not_consumed_cost", string.format("%.2fms", dt))
  193. end
  194. end
  195. return false
  196. end
  197. -- 系统初始化
  198. function core.init(opts)
  199. opts = opts or {}
  200. -- 主题设置:根据传入参数设置当前主题(light/dark)
  201. if opts.theme == "light" or opts.theme == "dark" then
  202. current_theme = opts.theme
  203. end
  204. -- 触摸事件订阅与转发(extp)
  205. -- extp_down_x, extp_down_y 记录按下时的原始坐标
  206. -- extp_curr_x, extp_curr_y 记录当前触摸点坐标(用于MOVE/滑动等)
  207. local extp_down_x, extp_down_y
  208. local extp_curr_x, extp_curr_y
  209. -- extp_dispatch: 触摸事件分发函数,负责将底层触摸事件转换为UI事件
  210. -- evt: 事件类型(如TOUCH_DOWN、MOVE_X、SINGLE_TAP等)
  211. -- a, b: 事件参数(如坐标或偏移量)
  212. local function extp_dispatch(evt, a, b)
  213. if evt == "TOUCH_DOWN" then
  214. -- 记录触摸按下时的原始坐标,a和b通常为触摸点的x、y坐标,若无法转换为数字则默认为0
  215. extp_down_x, extp_down_y = tonumber(a) or 0, tonumber(b) or 0
  216. extp_curr_x, extp_curr_y = extp_down_x, extp_down_y
  217. if core.debug_touch then log.info("exEasyUI", "extp", "TOUCH_DOWN", extp_curr_x, extp_curr_y) end
  218. -- 分发TOUCH_DOWN事件
  219. core.handleTouchEvent("TOUCH_DOWN", extp_curr_x, extp_curr_y)
  220. last_action = "TOUCH_DOWN"
  221. return
  222. end
  223. -- 若未按下则忽略后续事件
  224. if not extp_down_x or not extp_down_y then return end
  225. if evt == "MOVE_X" then
  226. -- 处理横向滑动,a为x方向偏移
  227. local dx = tonumber(a) or 0
  228. extp_curr_x = extp_down_x + dx
  229. if core.debug_touch then log.info("exEasyUI", "extp", "MOVE_X", extp_curr_x, extp_curr_y) end
  230. core.handleTouchEvent("MOVE_X", extp_curr_x, extp_curr_y)
  231. last_action = "MOVE_X"
  232. return
  233. elseif evt == "MOVE_Y" then
  234. -- 处理纵向滑动,b为y方向偏移
  235. local dy = tonumber(b) or 0
  236. extp_curr_y = extp_down_y + dy
  237. if core.debug_touch then log.info("exEasyUI", "extp", "MOVE_Y", extp_curr_x, extp_curr_y) end
  238. core.handleTouchEvent("MOVE_Y", extp_curr_x, extp_curr_y)
  239. last_action = "MOVE_Y"
  240. return
  241. elseif evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" then
  242. -- 处理左右滑动手势,a为x方向偏移
  243. local dx = tonumber(a) or 0
  244. extp_curr_x = extp_down_x + dx
  245. if core.debug_touch then log.info("exEasyUI", "extp", evt, extp_curr_x, extp_curr_y) end
  246. core.handleTouchEvent(evt, extp_curr_x, extp_curr_y)
  247. last_action = evt
  248. elseif evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
  249. -- 处理上下滑动手势,b为y方向偏移
  250. local dy = tonumber(b) or 0
  251. extp_curr_y = extp_down_y + dy
  252. if core.debug_touch then log.info("exEasyUI", "extp", evt, extp_curr_x, extp_curr_y) end
  253. core.handleTouchEvent(evt, extp_curr_x, extp_curr_y)
  254. last_action = evt
  255. elseif evt == "SINGLE_TAP" or evt == "LONG_PRESS" then
  256. -- 处理单击/长按事件,a/b为最终坐标(若无则用当前坐标)
  257. local ux = tonumber(a) or extp_curr_x or 0
  258. local uy = tonumber(b) or extp_curr_y or 0
  259. if core.debug_touch then log.info("exEasyUI", "extp", evt, ux, uy) end
  260. core.handleTouchEvent(evt, ux, uy)
  261. last_action = evt
  262. end
  263. -- 触摸序列结束后,清空坐标状态
  264. if last_action == "SINGLE_TAP" or last_action == "LONG_PRESS" or
  265. last_action == "SWIPE_LEFT" or last_action == "SWIPE_RIGHT" or
  266. last_action == "SWIPE_UP" or last_action == "SWIPE_DOWN" then
  267. extp_down_x, extp_down_y = nil, nil
  268. extp_curr_x, extp_curr_y = nil, nil
  269. end
  270. end
  271. -- 订阅底层触摸事件(baseTouchEvent),由extp_dispatch处理
  272. sys.subscribe("baseTouchEvent", extp_dispatch)
  273. end
  274. -- 获取当前主题
  275. function core.getTheme()
  276. return current_theme
  277. end
  278. -- 事件系统:订阅事件
  279. function event.on(name, cb)
  280. return sys.subscribe(name, cb)
  281. end
  282. -- 事件系统:发送事件
  283. function event.emit(name, ...)
  284. return sys.publish(name, ...)
  285. end
  286. -- ================================
  287. -- 4. 组件部分
  288. -- ================================
  289. -- 通用绘图函数
  290. local function fill_rect(x1, y1, x2, y2, color)
  291. lcd.fill(x1, y1, x2, y2 + 1, color) -- 右下边界为不含区间, y2需要+1
  292. end
  293. local function stroke_rect(x1, y1, x2, y2, color)
  294. lcd.drawLine(x1, y1, x2, y1, color)
  295. lcd.drawLine(x2, y1, x2, y2, color)
  296. lcd.drawLine(x2, y2, x1, y2, color)
  297. lcd.drawLine(x1, y2, x1, y1, color)
  298. end
  299. -- v1.6.1新增:绘制图片占位符(方框+X叉)
  300. local function draw_image_placeholder(x, y, w, h, bg_color, border_color)
  301. bg_color = bg_color or 0x8410 -- 默认灰色
  302. border_color = border_color or COLOR_WHITE
  303. -- 填充背景
  304. fill_rect(x, y, x + w - 1, y + h - 1, bg_color)
  305. -- 绘制边框
  306. stroke_rect(x, y, x + w - 1, y + h - 1, border_color)
  307. -- 绘制X叉(对角线)
  308. lcd.drawLine(x, y, x + w - 1, y + h - 1, border_color)
  309. lcd.drawLine(x + w - 1, y, x, y + h - 1, border_color)
  310. -- 如果尺寸足够大,绘制内缩的X叉使其更明显
  311. if w >= 20 and h >= 20 then
  312. local margin = math.min(w, h) // 8 -- 内缩边距
  313. lcd.drawLine(x + margin, y + margin, x + w - 1 - margin, y + h - 1 - margin, border_color)
  314. lcd.drawLine(x + w - 1 - margin, y + margin, x + margin, y + h - 1 - margin, border_color)
  315. end
  316. end
  317. -- FontAdapter 实现
  318. local function font_line_height(style)
  319. if FontAdapter._backend == "gtfont" then
  320. local sz = (style and style.size) or FontAdapter._size or 16
  321. return sz
  322. end
  323. -- default backend:优先使用样式中的 size,没有则回退 12
  324. if style and style.size then
  325. return tonumber(style.size) or 12
  326. end
  327. return 12
  328. end
  329. local function font_set(style)
  330. style = style or {}
  331. if FontAdapter._backend == "gtfont" then
  332. FontAdapter._size = tonumber(style.size or FontAdapter._size or 16)
  333. FontAdapter._gray = (style.gray ~= nil) and not not style.gray or FontAdapter._gray
  334. if lcd and lcd.setFont and lcd.drawGtfontUtf8 then
  335. lcd.setFont(lcd.drawGtfontUtf8)
  336. end
  337. return
  338. end
  339. -- default backend
  340. FontAdapter._name = style.name or FontAdapter._name
  341. if lcd and lcd.setFont then
  342. -- 优先按 name,其次按 size 猜测常见字体名,最后回退
  343. if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
  344. lcd.setFont(lcd["font_" .. FontAdapter._name])
  345. elseif style and style.size then
  346. local size_num = tonumber(style.size)
  347. if size_num then
  348. local guess = "font_opposansm" .. tostring(size_num) .. "_chinese"
  349. if lcd[guess] then
  350. lcd.setFont(lcd[guess])
  351. elseif lcd.font_opposansm12_chinese then
  352. lcd.setFont(lcd.font_opposansm12_chinese)
  353. end
  354. elseif lcd.font_opposansm12_chinese then
  355. lcd.setFont(lcd.font_opposansm12_chinese)
  356. end
  357. elseif lcd.font_opposansm12_chinese then
  358. lcd.setFont(lcd.font_opposansm12_chinese)
  359. end
  360. end
  361. end
  362. local function font_draw(text, x, y, color, style)
  363. color = color or COLOR_WHITE
  364. style = style or {}
  365. if FontAdapter._backend == "gtfont" then
  366. local sz = tonumber(style.size or FontAdapter._size or 16)
  367. if FontAdapter._gray and lcd.drawGtfontUtf8Gray then
  368. -- 固件灰度级目前不可调,传固定值4
  369. lcd.drawGtfontUtf8Gray(text, sz, 4, x, y, color)
  370. elseif lcd.drawGtfontUtf8 then
  371. lcd.drawGtfontUtf8(text, sz, x, y, color)
  372. else
  373. -- 回退:不应触达
  374. if lcd.drawStr then
  375. lcd.drawStr(x, y + 12, text, color)
  376. end
  377. end
  378. return
  379. end
  380. -- default backend:y 为顶部坐标,内部转换为基线
  381. if lcd and lcd.setFont then
  382. if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
  383. lcd.setFont(lcd["font_" .. FontAdapter._name])
  384. else
  385. -- 尝试根据 size 选择合适字体
  386. local used = false
  387. if style and style.size then
  388. local guess = "font_opposansm" .. tostring(style.size) .. "_chinese"
  389. if lcd[guess] then
  390. lcd.setFont(lcd[guess])
  391. used = true
  392. end
  393. end
  394. if not used and lcd.font_opposansm12_chinese then
  395. lcd.setFont(lcd.font_opposansm12_chinese)
  396. end
  397. end
  398. end
  399. local lh = font_line_height(style)
  400. lcd.drawStr(x, y + lh, text, color)
  401. end
  402. -- 文本宽度测量
  403. local function font_measure(text, style)
  404. if not text or text == "" then return 0 end
  405. style = style or {}
  406. if FontAdapter._backend == "gtfont" then
  407. local sz = tonumber(style.size or FontAdapter._size or 16)
  408. local w = 0
  409. local i = 1
  410. while i <= #text do
  411. local b = string.byte(text, i)
  412. if b == 32 then -- space
  413. w = w + math.ceil(sz / 2)
  414. i = i + 1
  415. elseif b < 128 then
  416. w = w + math.ceil(sz / 2)
  417. i = i + 1
  418. else
  419. w = w + sz
  420. -- 简化处理UTF-8宽字节
  421. if i + 2 <= #text then i = i + 3 else i = i + 1 end
  422. end
  423. end
  424. return w
  425. end
  426. -- default backend,尽量使用原生接口
  427. if lcd and lcd.setFont then
  428. -- 尝试在测量前设置到期望字体,以匹配绘制
  429. if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
  430. lcd.setFont(lcd["font_" .. FontAdapter._name])
  431. elseif style and style.size then
  432. local guess = "font_opposansm" .. tostring(style.size) .. "_chinese"
  433. if lcd[guess] then
  434. lcd.setFont(lcd[guess])
  435. elseif lcd.font_opposansm12_chinese then
  436. lcd.setFont(lcd.font_opposansm12_chinese)
  437. end
  438. elseif lcd.font_opposansm12_chinese then
  439. lcd.setFont(lcd.font_opposansm12_chinese)
  440. end
  441. end
  442. if lcd and lcd.getStrWidth then return lcd.getStrWidth(text) end
  443. if lcd and lcd.strWidth then return lcd.strWidth(text) end
  444. if lcd and lcd.get_string_width then return lcd.get_string_width(text) end
  445. -- 估算:英文约为 size/2,中文约为 size
  446. local width = 0
  447. local i = 1
  448. while i <= #text do
  449. local byte = string.byte(text, i)
  450. if byte < 128 then
  451. width = width + math.ceil((tonumber(style.size) or 12) / 2)
  452. i = i + 1
  453. else
  454. width = width + (tonumber(style.size) or 12)
  455. i = i + 3
  456. end
  457. end
  458. return width
  459. end
  460. -- UTF-8字符获取(返回字符和字节长度)
  461. local function get_utf8_char(text, i)
  462. if not text or i > #text then return "", 0 end
  463. local byte = string.byte(text, i)
  464. if byte < 128 then
  465. -- ASCII字符(1字节)
  466. return string.sub(text, i, i), 1
  467. elseif byte >= 224 and byte < 240 then
  468. -- 3字节UTF-8字符(中文等)
  469. if i + 2 <= #text then
  470. return string.sub(text, i, i + 2), 3
  471. else
  472. return string.sub(text, i, i), 1
  473. end
  474. elseif byte >= 192 and byte < 224 then
  475. -- 2字节UTF-8字符
  476. if i + 1 <= #text then
  477. return string.sub(text, i, i + 1), 2
  478. else
  479. return string.sub(text, i, i), 1
  480. end
  481. elseif byte >= 240 then
  482. -- 4字节UTF-8字符
  483. if i + 3 <= #text then
  484. return string.sub(text, i, i + 3), 4
  485. else
  486. return string.sub(text, i, i), 1
  487. end
  488. else
  489. -- 其他情况
  490. return string.sub(text, i, i), 1
  491. end
  492. end
  493. -- 文本换行处理(返回行数组)
  494. -- 支持英文按单词换行,中文按字符换行
  495. local function wrap_text_lines(text, maxWidth, style)
  496. if not text or text == "" then return {""} end
  497. if not maxWidth or maxWidth <= 0 then return {text} end
  498. local lines = {}
  499. local currentLine = ""
  500. local currentWidth = 0
  501. local wordBuffer = "" -- 当前英文单词缓冲
  502. local wordWidth = 0 -- 当前单词宽度
  503. local i = 1
  504. while i <= #text do
  505. local char, charLen = get_utf8_char(text, i)
  506. local charWidth = font_measure(char, style)
  507. local byte = string.byte(text, i)
  508. -- 判断是否为英文字母或数字
  509. local isAlphaNum = (byte >= 48 and byte <= 57) or -- 0-9
  510. (byte >= 65 and byte <= 90) or -- A-Z
  511. (byte >= 97 and byte <= 122) -- a-z
  512. if isAlphaNum then
  513. -- 英文字符或数字,加入单词缓冲
  514. wordBuffer = wordBuffer .. char
  515. wordWidth = wordWidth + charWidth
  516. i = i + charLen
  517. else
  518. -- 非英文字符(空格、标点、中文等)
  519. -- 先处理缓冲的单词
  520. if wordBuffer ~= "" then
  521. if currentWidth + wordWidth > maxWidth then
  522. -- 单词放不下
  523. if currentLine ~= "" then
  524. -- 当前行有内容,换行后放单词
  525. table.insert(lines, currentLine)
  526. currentLine = wordBuffer
  527. currentWidth = wordWidth
  528. else
  529. -- 单词本身超长,强制显示
  530. currentLine = wordBuffer
  531. currentWidth = wordWidth
  532. end
  533. else
  534. -- 单词可以放下
  535. currentLine = currentLine .. wordBuffer
  536. currentWidth = currentWidth + wordWidth
  537. end
  538. wordBuffer = ""
  539. wordWidth = 0
  540. end
  541. -- 处理当前字符(空格、标点、中文等)
  542. if char == " " then
  543. -- 空格:尝试加入当前行
  544. if currentWidth + charWidth <= maxWidth then
  545. currentLine = currentLine .. char
  546. currentWidth = currentWidth + charWidth
  547. else
  548. -- 空格放不下,换行(空格不放到下一行开头)
  549. if currentLine ~= "" then
  550. table.insert(lines, currentLine)
  551. end
  552. currentLine = ""
  553. currentWidth = 0
  554. end
  555. else
  556. -- 标点或中文
  557. if currentWidth + charWidth > maxWidth then
  558. -- 字符放不下,换行
  559. if currentLine ~= "" then
  560. table.insert(lines, currentLine)
  561. end
  562. currentLine = char
  563. currentWidth = charWidth
  564. else
  565. -- 字符可以放下
  566. currentLine = currentLine .. char
  567. currentWidth = currentWidth + charWidth
  568. end
  569. end
  570. i = i + charLen
  571. end
  572. end
  573. -- 处理剩余的单词
  574. if wordBuffer ~= "" then
  575. if currentWidth + wordWidth > maxWidth and currentLine ~= "" then
  576. -- 单词放不下,换行
  577. table.insert(lines, currentLine)
  578. currentLine = wordBuffer
  579. else
  580. currentLine = currentLine .. wordBuffer
  581. end
  582. end
  583. -- 添加最后一行
  584. if currentLine ~= "" then
  585. table.insert(lines, currentLine)
  586. end
  587. -- 至少返回一个空行
  588. if #lines == 0 then
  589. lines = {""}
  590. end
  591. return lines
  592. end
  593. -- 兼容旧接口已移除,统一使用下方两个新 API
  594. -- 新增:直接绘制文本 API(支持 style.size/name/gray)
  595. local function draw_text_direct(x, y, text, opts)
  596. opts = opts or {}
  597. local color = opts.color or COLOR_WHITE
  598. local style = opts.style or {}
  599. font_draw(text or "", x, y, color, style)
  600. end
  601. -- 新增:在矩形内自适应(仅居中与边界约束,不缩放、不换行)
  602. local function draw_text_in_rect_centered(x, y, w, h, text, opts)
  603. opts = opts or {}
  604. local color = opts.color or COLOR_WHITE
  605. local style = opts.style or {}
  606. local padding = opts.padding or 0
  607. local tw = font_measure(text or "", style)
  608. local lh = font_line_height(style)
  609. local inner_x = x + padding
  610. local inner_y = y + padding
  611. local inner_w = w - padding * 2
  612. local inner_h = h - padding * 2
  613. local tx = inner_x + (inner_w - tw) // 2
  614. local ty = inner_y + (inner_h - lh) // 2
  615. -- 夹紧,避免越界
  616. tx = math.max(inner_x, tx)
  617. tx = math.min(inner_x + inner_w - tw, tx)
  618. font_draw(text or "", tx, ty, color, style)
  619. end
  620. -- 旧的宽度测量包装已移除,请直接使用 font_measure(text, style)
  621. -- Button组件 - 基础按钮,支持文本/图片、按下/抬起与点击回调,支持toggle模式
  622. local Button = {}
  623. Button.__index = Button
  624. function Button:new(opts)
  625. opts = opts or {}
  626. local o = setmetatable({}, self)
  627. o.x = opts.x or 0
  628. o.y = opts.y or 0
  629. o.w = opts.width or opts.w or 100
  630. o.h = opts.height or opts.h or 36
  631. -- 文本模式参数
  632. o.text = opts.text or "Button"
  633. o.textSize = opts.textSize or opts.size
  634. local dark = (current_theme == "dark")
  635. o.bgColor = opts.bgColor or (dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG)
  636. o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
  637. o.borderColor = opts.borderColor or (dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER)
  638. -- 图片模式参数(v1.6.0新增,合并自ToolButton)
  639. o.src = opts.src
  640. o.src_pressed = opts.src_pressed
  641. o.src_toggled = opts.src_toggled
  642. -- Toggle模式参数(v1.6.0新增)
  643. o.toggle = opts.toggle or false
  644. o.toggled = opts.toggled or false
  645. o.onToggle = opts.onToggle
  646. -- 状态
  647. o.pressed = false
  648. o.onClick = opts.onClick
  649. o.visible = opts.visible ~= false
  650. o.enabled = opts.enabled ~= false
  651. o._imageCache = {} -- v1.6.1:缓存图片加载状态,避免重复检查和重复打印警告
  652. return o
  653. end
  654. function Button:draw()
  655. if not self.visible then return end
  656. -- 图片模式:优先显示图片
  657. if self.src then
  658. local path
  659. if self.toggle and self.toggled then
  660. path = self.src_toggled or self.src
  661. elseif self.pressed then
  662. path = self.src_pressed or self.src
  663. else
  664. path = self.src
  665. end
  666. -- v1.6.1修复:检查文件是否存在,并改进占位符显示(使用缓存避免重复检查)
  667. if type(path) == "string" and path ~= "" and path:lower():sub(-4) == ".jpg" then
  668. -- 检查缓存
  669. if self._imageCache[path] == nil then
  670. -- 未缓存,首次检查文件是否存在
  671. if io and io.exists and io.exists(path) then
  672. self._imageCache[path] = true -- 缓存:文件存在
  673. else
  674. self._imageCache[path] = false -- 缓存:文件不存在
  675. log.warn("Button", "图片文件不存在:", path)
  676. end
  677. end
  678. -- 根据缓存状态处理
  679. if self._imageCache[path] == true then
  680. lcd.showImage(self.x, self.y, path)
  681. else
  682. -- 文件不存在(已缓存),直接显示占位符,不再重复警告
  683. draw_image_placeholder(self.x, self.y, self.w, self.h, COLOR_GRAY, COLOR_WHITE)
  684. end
  685. else
  686. -- path无效或不是jpg,显示占位符
  687. draw_image_placeholder(self.x, self.y, self.w, self.h, COLOR_GRAY, COLOR_WHITE)
  688. end
  689. return
  690. end
  691. -- 文本模式:绘制文本按钮
  692. local bg = self.pressed and COLOR_GRAY or self.bgColor
  693. fill_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, bg)
  694. stroke_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.borderColor)
  695. draw_text_in_rect_centered(self.x, self.y, self.w, self.h, self.text, {
  696. color = self.textColor,
  697. style = { size = self.textSize },
  698. padding = 2
  699. })
  700. end
  701. function Button:setText(newText)
  702. self.text = tostring(newText or "")
  703. end
  704. function Button:handleEvent(evt, x, y)
  705. if not self.enabled then return false end
  706. local inside = hit_test(x, y, { x = self.x, y = self.y, w = self.w, h = self.h })
  707. if evt == "TOUCH_DOWN" and inside then
  708. self.pressed = true
  709. self._capture = true
  710. return true
  711. elseif evt == "MOVE_X" or evt == "MOVE_Y" then
  712. if self._capture then
  713. self.pressed = inside
  714. return true
  715. end
  716. elseif evt == "SINGLE_TAP" then
  717. local was_pressed = self.pressed
  718. self.pressed = false
  719. self._capture = false
  720. if was_pressed and inside then
  721. -- Toggle模式处理
  722. if self.toggle then
  723. self.toggled = not self.toggled
  724. if self.onToggle then self.onToggle(self.toggled, self) end
  725. end
  726. -- 触发点击回调
  727. if self.onClick then self.onClick(self) end
  728. return true
  729. end
  730. return true
  731. elseif evt == "LONG_PRESS" or evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" or evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
  732. self.pressed = false
  733. self._capture = false
  734. return true
  735. end
  736. return false
  737. end
  738. -- CheckBox组件 - 复选框,支持选中状态切换
  739. local CheckBox = {}
  740. CheckBox.__index = CheckBox
  741. function CheckBox:new(opts)
  742. opts = opts or {}
  743. local o = setmetatable({}, self)
  744. o.x = opts.x or 0
  745. o.y = opts.y or 0
  746. o.boxSize = opts.boxSize or 16
  747. o.text = opts.text
  748. local dark = (current_theme == "dark")
  749. o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
  750. o.borderColor = opts.borderColor or (dark and COLOR_WHITE or COLOR_BLACK)
  751. o.bgColor = opts.bgColor or (dark and COLOR_BLACK or COLOR_WHITE)
  752. o.tickColor = opts.tickColor or (dark and COLOR_WHITE or COLOR_BLACK)
  753. o.checked = opts.checked or false
  754. o.enabled = opts.enabled ~= false
  755. o.visible = opts.visible ~= false
  756. o.onChange = opts.onChange
  757. local text_w = (o.text and (#o.text * 6 + 6) or 0)
  758. o.w = o.boxSize + text_w
  759. o.h = math.max(o.boxSize, 16)
  760. return o
  761. end
  762. function CheckBox:draw()
  763. local x2 = self.x + self.boxSize - 1
  764. local y2 = self.y + self.boxSize - 1
  765. stroke_rect(self.x, self.y, x2, y2, self.borderColor)
  766. fill_rect(self.x + 2, self.y + 2, x2 - 2, y2 - 2, self.bgColor)
  767. if self.checked then
  768. local pad = 2
  769. fill_rect(self.x + pad, self.y + pad, x2 - pad, y2 - pad, self.tickColor)
  770. end
  771. if self.text then
  772. local lh = font_line_height(nil)
  773. local ty = self.y + (self.h - lh) // 2
  774. draw_text_direct(self.x + self.boxSize + 10, ty, self.text, { color = self.textColor })
  775. end
  776. end
  777. function CheckBox:setChecked(v)
  778. local nv = not not v
  779. if nv ~= self.checked then
  780. self.checked = nv
  781. if self.onChange then self.onChange(self.checked) end
  782. end
  783. end
  784. function CheckBox:toggle()
  785. self:setChecked(not self.checked)
  786. end
  787. function CheckBox:handleEvent(evt, x, y)
  788. if not self.enabled then return false end
  789. if evt == "SINGLE_TAP" then
  790. if x >= self.x and y >= self.y and x <= (self.x + self.w) and y <= (self.y + self.h) then
  791. self:toggle()
  792. return true
  793. end
  794. end
  795. return false
  796. end
  797. -- Label组件 - 文本标签,仅显示不响应事件
  798. local Label = {}
  799. Label.__index = Label
  800. function Label:new(opts)
  801. opts = opts or {}
  802. local o = setmetatable({}, self)
  803. o.x = opts.x or 0
  804. o.y = opts.y or 0
  805. o.text = tostring(opts.text or "")
  806. local dark = (current_theme == "dark")
  807. o.color = opts.color or (dark and COLOR_WHITE or COLOR_BLACK)
  808. o.font = opts.font
  809. o.size = opts.size or opts.textSize
  810. o.wordWrap = not not opts.wordWrap -- 是否启用换行
  811. o._autoW = (opts.w == nil)
  812. o._autoH = true -- 高度始终自动计算
  813. o.visible = opts.visible ~= false
  814. o.enabled = opts.enabled ~= false
  815. -- 宽度处理
  816. local style = { size = o.size }
  817. if opts.w then
  818. o.w = opts.w
  819. o._autoW = false
  820. else
  821. o.w = font_measure(o.text, style)
  822. o._autoW = true
  823. end
  824. -- 高度处理(根据是否换行)
  825. local lh = font_line_height(style)
  826. if o.wordWrap and not o._autoW then
  827. -- 启用换行且指定了宽度,计算多行高度
  828. local lines = wrap_text_lines(o.text, o.w, style)
  829. o.h = #lines * lh
  830. o._lines = lines
  831. else
  832. -- 单行高度
  833. o.h = lh
  834. o._lines = nil
  835. end
  836. return o
  837. end
  838. function Label:setText(t)
  839. self.text = tostring(t or "")
  840. local style = { size = self.size }
  841. local lh = font_line_height(style)
  842. -- 更新宽度(如果自动)
  843. if self._autoW then
  844. self.w = font_measure(self.text, style)
  845. end
  846. -- 更新高度和行缓存
  847. if self.wordWrap and not self._autoW then
  848. local lines = wrap_text_lines(self.text, self.w, style)
  849. self.h = #lines * lh
  850. self._lines = lines
  851. else
  852. self.h = lh
  853. self._lines = nil
  854. end
  855. end
  856. function Label:setSize(sz)
  857. self.size = tonumber(sz) or self.size
  858. local style = { size = self.size }
  859. local lh = font_line_height(style)
  860. -- 更新宽度(如果自动)
  861. if self._autoW then
  862. self.w = font_measure(self.text or "", style)
  863. end
  864. -- 更新高度和行缓存
  865. if self.wordWrap and not self._autoW then
  866. local lines = wrap_text_lines(self.text or "", self.w, style)
  867. self.h = #lines * lh
  868. self._lines = lines
  869. else
  870. self.h = lh
  871. self._lines = nil
  872. end
  873. end
  874. function Label:draw()
  875. if not self.visible then return end
  876. local style = { size = self.size }
  877. -- 若指定自定义字体指针,走默认后端路径(不支持换行)
  878. if self.font and lcd and lcd.setFont then
  879. lcd.setFont(self.font)
  880. local lh = font_line_height(nil)
  881. lcd.drawStr(self.x, self.y + lh, self.text, self.color)
  882. return
  883. end
  884. -- 换行模式
  885. if self.wordWrap and not self._autoW then
  886. local lines = self._lines or wrap_text_lines(self.text, self.w, style)
  887. local lh = font_line_height(style)
  888. for i = 1, #lines do
  889. local yPos = self.y + (i - 1) * lh
  890. draw_text_direct(self.x, yPos, lines[i], { color = self.color, style = style })
  891. end
  892. return
  893. end
  894. -- 无换行模式:截断显示
  895. if not self._autoW then
  896. -- 有指定宽度限制,需要截断
  897. local displayText = self.text
  898. local tw = font_measure(displayText, style)
  899. if tw > self.w then
  900. -- 逐字符截断,直到宽度合适
  901. local truncated = ""
  902. local i = 1
  903. while i <= #displayText do
  904. local char, charLen = get_utf8_char(displayText, i)
  905. local testText = truncated .. char
  906. if font_measure(testText, style) <= self.w then
  907. truncated = testText
  908. i = i + charLen
  909. else
  910. break
  911. end
  912. end
  913. displayText = truncated
  914. end
  915. draw_text_direct(self.x, self.y, displayText, { color = self.color, style = style })
  916. else
  917. -- 自动宽度,直接显示
  918. draw_text_direct(self.x, self.y, self.text, { color = self.color, style = style })
  919. end
  920. end
  921. function Label:handleEvent(evt, x, y)
  922. return false -- 文本不拦截事件
  923. end
  924. -- Picture组件 - 显示单图或轮播多图
  925. local Picture = {}
  926. Picture.__index = Picture
  927. function Picture:new(opts)
  928. opts = opts or {}
  929. local o = setmetatable({}, self)
  930. o.x = opts.x or 0
  931. o.y = opts.y or 0
  932. o.w = opts.w or 80
  933. o.h = opts.h or 80
  934. o.src = opts.src
  935. o.sources = opts.sources
  936. o.index = opts.index or 1
  937. o.autoplay = not not opts.autoplay
  938. o.interval = opts.interval or 1000
  939. o._last_switch = now_ms()
  940. o.visible = opts.visible ~= false
  941. o.enabled = opts.enabled ~= false
  942. o._imageCache = {} -- v1.6.1:缓存图片加载状态,避免重复检查和重复打印警告
  943. return o
  944. end
  945. function Picture:setSources(list)
  946. self.sources = list
  947. self.index = 1
  948. end
  949. function Picture:next()
  950. if self.sources and #self.sources > 0 then
  951. self.index = self.index % #self.sources + 1
  952. end
  953. end
  954. function Picture:prev()
  955. if self.sources and #self.sources > 0 then
  956. self.index = (self.index - 2) % #self.sources + 1
  957. end
  958. end
  959. function Picture:play()
  960. self.autoplay = true
  961. end
  962. function Picture:pause()
  963. self.autoplay = false
  964. end
  965. function Picture:draw()
  966. if not self.visible then return end
  967. -- 自动轮播
  968. if self.autoplay and self.sources and #self.sources > 1 then
  969. local t = now_ms()
  970. if (t - self._last_switch) >= self.interval then
  971. self:next()
  972. self._last_switch = t
  973. end
  974. end
  975. -- 选择当前图片路径
  976. local path
  977. if self.sources and #self.sources > 0 then
  978. path = self.sources[self.index]
  979. else
  980. path = self.src
  981. end
  982. -- v1.6.1修复:检查文件是否存在,并改进占位符显示(使用缓存避免重复检查)
  983. if type(path) == "string" and path ~= "" and path:lower():sub(-4) == ".jpg" then
  984. -- 检查缓存
  985. if self._imageCache[path] == nil then
  986. -- 未缓存,首次检查文件是否存在
  987. if io and io.exists and io.exists(path) then
  988. self._imageCache[path] = true -- 缓存:文件存在
  989. else
  990. self._imageCache[path] = false -- 缓存:文件不存在
  991. log.warn("Picture", "图片文件不存在:", path)
  992. end
  993. end
  994. -- 根据缓存状态处理
  995. if self._imageCache[path] == true then
  996. lcd.showImage(self.x, self.y, path)
  997. else
  998. -- 文件不存在(已缓存),显示占位符
  999. draw_image_placeholder(self.x, self.y, self.w, self.h, 0x4208, COLOR_WHITE)
  1000. end
  1001. elseif path then
  1002. -- path不是jpg或无效路径,显示占位符
  1003. draw_image_placeholder(self.x, self.y, self.w, self.h, 0x4208, COLOR_WHITE)
  1004. end
  1005. -- 如果path为nil,不显示任何内容(不绘制占位符)
  1006. end
  1007. function Picture:handleEvent(evt, x, y)
  1008. return false -- 默认不消费事件
  1009. end
  1010. -- MessageBox组件 - 消息框,包含标题、文本和按钮组
  1011. local MessageBox = {}
  1012. MessageBox.__index = MessageBox
  1013. function MessageBox:new(opts)
  1014. opts = opts or {}
  1015. local o = setmetatable({}, self)
  1016. o.x = opts.x or 20
  1017. o.y = opts.y or 40
  1018. o.w = opts.width or opts.w or 280
  1019. o.h = opts.height or opts.h or 160
  1020. o.title = opts.title or "Info"
  1021. o.message = opts.message or ""
  1022. o.wordWrap = opts.wordWrap ~= false -- v1.6.1修复:默认启用自动换行,除非显式传入false
  1023. o.textSize = opts.textSize or opts.size -- 文本字号
  1024. local dark = (current_theme == "dark")
  1025. o.borderColor = opts.borderColor or (dark and COLOR_WHITE or COLOR_BLACK)
  1026. o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
  1027. o.bgColor = opts.bgColor or (dark and COLOR_BLACK or COLOR_WHITE)
  1028. o.buttons = opts.buttons or { "OK" }
  1029. o.onResult = opts.onResult
  1030. o.visible = opts.visible ~= false -- v1.6.1修复:支持从opts读取visible参数,默认true
  1031. o.enabled = opts.enabled ~= false -- v1.6.1修复:支持从opts读取enabled参数,默认true
  1032. -- 内部按钮布局
  1033. o._btns = {}
  1034. local btn_w = 80
  1035. local gap = 12
  1036. local total_w = #o.buttons * btn_w + (#o.buttons - 1) * gap
  1037. local bx = o.x + (o.w - total_w) // 2
  1038. local by = o.y + o.h - 12 - 36
  1039. for i = 1, #o.buttons do
  1040. local label = o.buttons[i]
  1041. local b = Button:new({ x = bx, y = by, w = btn_w, h = 36, text = label })
  1042. b.onClick = function()
  1043. if o.onResult then o.onResult(label) end
  1044. o.visible = false
  1045. -- v1.6.1修复:不再禁用enabled,允许MessageBox复用
  1046. end
  1047. o._btns[#o._btns + 1] = b
  1048. bx = bx + btn_w + gap
  1049. end
  1050. -- 计算message文本可用区域
  1051. o._msgPadding = 10 -- 左右内边距
  1052. o._msgMaxWidth = o.w - o._msgPadding * 2
  1053. o._msgStartY = 36 -- message文本起始Y(相对于MessageBox)
  1054. -- v1.6.1修复:根据是否有按钮动态计算可用高度
  1055. if #o.buttons > 0 then
  1056. o._msgMaxHeight = o.h - 12 - 36 - o._msgStartY -- 有按钮:预留底部边距12 + 按钮高度36
  1057. else
  1058. o._msgMaxHeight = o.h - 10 - o._msgStartY -- 无按钮:只保留底部边距10
  1059. end
  1060. return o
  1061. end
  1062. function MessageBox:draw()
  1063. if not self.visible then return end
  1064. fill_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.bgColor)
  1065. stroke_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.borderColor)
  1066. -- 绘制标题
  1067. draw_text_direct(self.x + 10, self.y + 8, self.title, { color = self.textColor, style = { size = self.textSize } })
  1068. -- 绘制message文本
  1069. local msgX = self.x + self._msgPadding
  1070. local msgY = self.y + self._msgStartY
  1071. local style = { size = self.textSize }
  1072. if self.wordWrap then
  1073. -- 换行模式:在固定高度内显示多行,超出截断
  1074. local lines = wrap_text_lines(self.message, self._msgMaxWidth, style)
  1075. local lh = font_line_height(style)
  1076. local maxLines = math.floor(self._msgMaxHeight / lh)
  1077. for i = 1, math.min(#lines, maxLines) do
  1078. local yPos = msgY + (i - 1) * lh
  1079. draw_text_direct(msgX, yPos, lines[i], { color = self.textColor, style = style })
  1080. end
  1081. else
  1082. -- 无换行模式:单行显示
  1083. draw_text_direct(msgX, msgY, self.message, { color = self.textColor, style = style })
  1084. end
  1085. -- 绘制按钮
  1086. for i = 1, #self._btns do
  1087. self._btns[i]:draw()
  1088. end
  1089. end
  1090. function MessageBox:handleEvent(evt, x, y)
  1091. if not self.enabled then return false end
  1092. for i = 1, #self._btns do
  1093. local b = self._btns[i]
  1094. if hit_test(x, y, { x = b.x, y = b.y, w = b.w, h = b.h }) then
  1095. return b:handleEvent(evt, x, y)
  1096. end
  1097. end
  1098. return true -- 拦截其它事件
  1099. end
  1100. -- v1.6.1新增:MessageBox复用方法
  1101. function MessageBox:show()
  1102. self.visible = true
  1103. self.enabled = true
  1104. end
  1105. function MessageBox:hide()
  1106. self.visible = false
  1107. end
  1108. function MessageBox:setTitle(title)
  1109. self.title = tostring(title or "")
  1110. end
  1111. function MessageBox:setMessage(message)
  1112. self.message = tostring(message or "")
  1113. -- 如果启用了换行,更新行缓存
  1114. if self.wordWrap then
  1115. local style = { size = self.textSize }
  1116. self._lines = wrap_text_lines(self.message, self._msgMaxWidth, style)
  1117. end
  1118. end
  1119. -- Window组件 - 窗口容器,支持子组件管理和子页面导航
  1120. local Window = {}
  1121. Window.__index = Window
  1122. function Window:new(opts)
  1123. opts = opts or {}
  1124. local o = setmetatable({}, self)
  1125. local sw, sh = lcd.getSize()
  1126. o.x = opts.x or 0
  1127. o.y = opts.y or 0
  1128. o.w = opts.w or sw
  1129. o.h = opts.h or sh
  1130. o.backgroundImage = opts.backgroundImage
  1131. local dark = (current_theme == "dark")
  1132. o.backgroundColor = opts.backgroundColor or (dark and COLOR_WIN11_DARK_DIALOG_BG or COLOR_WIN11_LIGHT_DIALOG_BG)
  1133. o.children = {}
  1134. o.visible = opts.visible ~= false
  1135. o.enabled = opts.enabled ~= false
  1136. o._managed = nil
  1137. o:enableSubpageManager()
  1138. -- 滚动配置(0.1 版:纵向/横向)
  1139. o._scroll = nil
  1140. return o
  1141. end
  1142. function Window:add(child)
  1143. self.children[#self.children + 1] = child
  1144. end
  1145. function Window:remove(child)
  1146. for i = #self.children, 1, -1 do
  1147. if self.children[i] == child then
  1148. table.remove(self.children, i)
  1149. return true
  1150. end
  1151. end
  1152. return false
  1153. end
  1154. function Window:clear()
  1155. self.children = {}
  1156. end
  1157. function Window:setBackgroundImage(path)
  1158. self.backgroundImage = path
  1159. end
  1160. function Window:setBackgroundColor(color)
  1161. self.backgroundColor = color
  1162. self.backgroundImage = nil
  1163. end
  1164. function Window:draw()
  1165. -- 背景
  1166. if self.backgroundImage then
  1167. lcd.showImage(self.x, self.y, self.backgroundImage)
  1168. else
  1169. lcd.fill(self.x, self.y, self.x + self.w, self.y + self.h, self.backgroundColor)
  1170. end
  1171. -- 子组件
  1172. local offX, offY = 0, 0
  1173. if self._scroll and self._scroll.enabled then
  1174. if self._scroll.direction == "vertical" then
  1175. offY = self._scroll.offsetY or 0
  1176. elseif self._scroll.direction == "horizontal" then
  1177. offX = self._scroll.offsetX or 0
  1178. elseif self._scroll.direction == "both" then
  1179. offX = self._scroll.offsetX or 0
  1180. offY = self._scroll.offsetY or 0
  1181. end
  1182. end
  1183. for i = 1, #self.children do
  1184. local c = self.children[i]
  1185. if c and c.visible ~= false and c.draw then
  1186. local ox, oy = c.x, c.y
  1187. if self._scroll and self._scroll.enabled then c.x = ox + offX c.y = oy + offY end
  1188. c:draw()
  1189. if self._scroll and self._scroll.enabled then c.x, c.y = ox, oy end
  1190. end
  1191. end
  1192. end
  1193. function Window:handleEvent(evt, x, y)
  1194. if not self.enabled then return false end
  1195. if not hit_test(x, y, { x = self.x, y = self.y, w = self.w, h = self.h }) then return false end
  1196. -- 简易滚动(0.1):vertical/horizontal
  1197. if self._scroll and self._scroll.enabled then
  1198. local sc = self._scroll
  1199. local contentW = sc.contentWidth or self.w
  1200. local contentH = sc.contentHeight or self.h
  1201. local minX = math.min(0, self.w - (contentW or self.w))
  1202. local maxX = 0
  1203. local minY = math.min(0, self.h - (contentH or self.h))
  1204. local maxY = 0
  1205. if evt == "TOUCH_DOWN" then
  1206. sc.startX = x
  1207. sc.startY = y
  1208. sc.baseOffsetX = sc.offsetX or 0
  1209. sc.baseOffsetY = sc.offsetY or 0
  1210. sc.dragging = false
  1211. sc.captured = false
  1212. -- 透传按下给命中的子组件,便于组件进入按下态;若后续进入拖拽会被取消
  1213. local tx = x - (sc.offsetX or 0)
  1214. local ty = y - (sc.offsetY or 0)
  1215. sc.downTarget = nil
  1216. for i = #self.children, 1, -1 do
  1217. local c = self.children[i]
  1218. if c and c.enabled ~= false and c.handleEvent and
  1219. hit_test(tx, ty, { x = c.x, y = c.y, w = c.w, h = c.h }) then
  1220. sc.downTarget = c
  1221. c:handleEvent("TOUCH_DOWN", tx, ty)
  1222. break
  1223. end
  1224. end
  1225. return true
  1226. elseif evt == "MOVE_Y" or evt == "MOVE_X" then
  1227. local dx = (x - (sc.startX or x))
  1228. local dy = (y - (sc.startY or y))
  1229. if not sc.dragging then
  1230. local m = math.max(math.abs(dx), math.abs(dy))
  1231. if m >= (sc.threshold or 10) then
  1232. sc.dragging = true
  1233. sc.captured = true
  1234. -- 进入拖拽,取消先前按下态
  1235. if sc.downTarget and sc.downTarget.handleEvent then
  1236. local tx = x - (sc.offsetX or 0)
  1237. local ty = y - (sc.offsetY or 0)
  1238. sc.downTarget:handleEvent("LONG_PRESS", tx, ty)
  1239. end
  1240. sc.downTarget = nil
  1241. else
  1242. -- v1.6.1修复:未达拖拽阈值时(观望期),转发MOVE给downTarget让其实时更新状态
  1243. if sc.downTarget and sc.downTarget.handleEvent then
  1244. local tx = x - (sc.offsetX or 0)
  1245. local ty = y - (sc.offsetY or 0)
  1246. sc.downTarget:handleEvent(evt, tx, ty)
  1247. end
  1248. end
  1249. end
  1250. if sc.dragging then
  1251. local nx = sc.baseOffsetX + dx
  1252. local ny = sc.baseOffsetY + dy
  1253. if sc.direction == "vertical" then
  1254. if ny < minY then ny = minY end
  1255. if ny > maxY then ny = maxY end
  1256. sc.offsetY = ny
  1257. elseif sc.direction == "horizontal" then
  1258. if nx < minX then nx = minX end
  1259. if nx > maxX then nx = maxX end
  1260. sc.offsetX = nx
  1261. else -- both
  1262. if nx < minX then nx = minX end
  1263. if nx > maxX then nx = maxX end
  1264. if ny < minY then ny = minY end
  1265. if ny > maxY then ny = maxY end
  1266. sc.offsetX, sc.offsetY = nx, ny
  1267. end
  1268. return true
  1269. end
  1270. return true
  1271. elseif evt == "SINGLE_TAP" or evt == "LONG_PRESS" then
  1272. if sc.dragging then
  1273. -- 滑动期间禁用点击;若启用分页,则在抬手时做“就近吸附”(无论是否触发 SWIPE)
  1274. if sc.pagingEnabled and (sc.direction == "horizontal" or sc.direction == "both") then
  1275. local pageW = sc.pageWidth or self.w
  1276. local totalW = sc.contentWidth or self.w
  1277. local pages = math.max(1, math.floor((totalW + pageW - 1) / pageW))
  1278. local cur = math.floor((-(sc.offsetX or 0) + pageW / 2) / pageW)
  1279. if cur < 0 then cur = 0 end
  1280. if cur > pages - 1 then cur = pages - 1 end
  1281. sc.offsetX = -cur * pageW
  1282. end
  1283. sc.dragging = false
  1284. sc.captured = false
  1285. sc.downTarget = nil
  1286. return true
  1287. end
  1288. -- 未拖拽:将事件分发给子组件(坐标转内容坐标)
  1289. local tx = x - (sc.offsetX or 0)
  1290. local ty = y - (sc.offsetY or 0)
  1291. for i = #self.children, 1, -1 do
  1292. local c = self.children[i]
  1293. if c and c.enabled ~= false and c.handleEvent and
  1294. hit_test(tx, ty, { x = c.x, y = c.y, w = c.w, h = c.h }) then
  1295. if c:handleEvent(evt, tx, ty) then sc.downTarget = nil return true end
  1296. end
  1297. end
  1298. sc.downTarget = nil
  1299. return true
  1300. elseif evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" or evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
  1301. -- 抬手后的滑动手势:结束拖拽,并在需要时做分页吸附(仅横向)
  1302. sc.dragging = false
  1303. sc.captured = false
  1304. if sc.pagingEnabled and (sc.direction == "horizontal" or sc.direction == "both") then
  1305. local pageW = sc.pageWidth or self.w
  1306. local totalW = sc.contentWidth or self.w
  1307. local pages = math.max(1, math.floor((totalW + pageW - 1) / pageW))
  1308. -- 当前页(offsetX 为负值向右移动内容)
  1309. local cur = math.floor((-(sc.offsetX or 0) + pageW / 2) / pageW)
  1310. if evt == "SWIPE_LEFT" then cur = cur + 1 elseif evt == "SWIPE_RIGHT" then cur = cur - 1 end
  1311. if cur < 0 then cur = 0 end
  1312. if cur > pages - 1 then cur = pages - 1 end
  1313. sc.offsetX = -cur * pageW
  1314. end
  1315. return true
  1316. else
  1317. -- 其他事件(如 MOVE_X/SWIPE_*)在 0.1 版忽略或按需拦截
  1318. return true
  1319. end
  1320. end
  1321. -- 非滚动窗口:正常分发
  1322. for i = #self.children, 1, -1 do
  1323. local c = self.children[i]
  1324. -- v1.6.1修复:添加_capture检查,让已捕获的组件(如按下的Button)能收到移出范围的MOVE事件
  1325. if c and c.enabled ~= false and c.handleEvent and
  1326. (c._capture == true or hit_test(x, y, { x = c.x, y = c.y, w = c.w, h = c.h })) then
  1327. if c:handleEvent(evt, x, y) then return true end
  1328. end
  1329. end
  1330. return true -- 拦截窗口区域内未被子组件消费的事件
  1331. end
  1332. -- 启用简易滚动(0.1)
  1333. function Window:enableScroll(opts)
  1334. opts = opts or {}
  1335. self._scroll = {
  1336. enabled = true,
  1337. direction = opts.direction or "vertical",
  1338. contentWidth = tonumber(opts.contentWidth or self.w) or self.w,
  1339. contentHeight = tonumber(opts.contentHeight or self.h) or self.h,
  1340. offsetX = 0,
  1341. offsetY = 0,
  1342. threshold = tonumber(opts.threshold or 10) or 10,
  1343. pagingEnabled = not not opts.pagingEnabled,
  1344. pageWidth = tonumber(opts.pageWidth or self.w) or self.w,
  1345. dragging = false,
  1346. captured = false,
  1347. }
  1348. return self
  1349. end
  1350. function Window:setContentSize(w, h)
  1351. if not self._scroll then return end
  1352. if w then self._scroll.contentWidth = tonumber(w) or self._scroll.contentWidth end
  1353. if h then self._scroll.contentHeight = tonumber(h) or self._scroll.contentHeight end
  1354. end
  1355. -- 启用子页面管理
  1356. function Window:enableSubpageManager(opts)
  1357. opts = opts or {}
  1358. if not self._managed then
  1359. self._managed = {
  1360. pages = {},
  1361. backEventName = opts.backEventName or "NAV.BACK",
  1362. onBack = opts.onBack
  1363. }
  1364. sys.subscribe(self._managed.backEventName, function()
  1365. if self._managed.onBack then pcall(self._managed.onBack) end
  1366. local anyVisible = false
  1367. for _, pg in pairs(self._managed.pages) do
  1368. if pg and pg.visible ~= false then anyVisible = true break end
  1369. end
  1370. if not anyVisible then
  1371. self.visible = true
  1372. self.enabled = true
  1373. end
  1374. end)
  1375. end
  1376. return self
  1377. end
  1378. -- 配置子页面工厂
  1379. function Window:configureSubpages(factories)
  1380. if not self._managed then self:enableSubpageManager() end
  1381. self._managed.factories = self._managed.factories or {}
  1382. for k, v in pairs(factories or {}) do
  1383. self._managed.factories[k] = v
  1384. end
  1385. return self
  1386. end
  1387. -- 显示子页面
  1388. function Window:showSubpage(name, factory)
  1389. if not self._managed then error("enableSubpageManager must be called before showSubpage") end
  1390. for key, pg in pairs(self._managed.pages) do
  1391. if pg and pg.visible ~= false then
  1392. pg.visible = false
  1393. pg.enabled = false
  1394. end
  1395. end
  1396. if not self._managed.pages[name] then
  1397. local f = factory
  1398. if not f and self._managed.factories then f = self._managed.factories[name] end
  1399. if not f then error("no factory for subpage '" .. tostring(name) .. "'") end
  1400. self._managed.pages[name] = f()
  1401. self._managed.pages[name]._parentWindow = self
  1402. core.add(self._managed.pages[name])
  1403. end
  1404. self.visible = false
  1405. self._managed.pages[name].visible = true
  1406. self._managed.pages[name].enabled = true
  1407. end
  1408. -- 返回上级页面
  1409. function Window:back()
  1410. if self._parentWindow then
  1411. self.visible = false
  1412. self.enabled = false
  1413. local parent = self._parentWindow
  1414. local anyVisible = false
  1415. if parent._managed and parent._managed.pages then
  1416. for _, pg in pairs(parent._managed.pages) do
  1417. if pg and pg.visible ~= false then anyVisible = true break end
  1418. end
  1419. end
  1420. if not anyVisible then
  1421. parent.visible = true
  1422. parent.enabled = true
  1423. end
  1424. end
  1425. end
  1426. -- 关闭子页面
  1427. function Window:closeSubpage(name, opts)
  1428. if not self._managed or not self._managed.pages then return false end
  1429. opts = opts or {}
  1430. local pg = self._managed.pages[name]
  1431. if not pg then return false end
  1432. pg.visible = false
  1433. pg.enabled = false
  1434. if opts.destroy == true then
  1435. core.remove(pg)
  1436. self._managed.pages[name] = nil
  1437. collectgarbage("collect")
  1438. end
  1439. local anyVisible = false
  1440. for _, p in pairs(self._managed.pages) do
  1441. if p and p.visible ~= false then anyVisible = true break end
  1442. end
  1443. if not anyVisible then
  1444. self.visible = true
  1445. self.enabled = true
  1446. end
  1447. return true
  1448. end
  1449. -- ProgressBar组件 - 进度条,支持百分比显示和主题适配
  1450. local ProgressBar = {}
  1451. ProgressBar.__index = ProgressBar
  1452. function ProgressBar:new(opts)
  1453. opts = opts or {}
  1454. local o = setmetatable({}, self)
  1455. o.x = opts.x or 0
  1456. o.y = opts.y or 0
  1457. o.w = opts.width or opts.w or 200
  1458. o.h = opts.height or opts.h or 24
  1459. o.progress = math.max(0, math.min(100, opts.progress or 0))
  1460. o.showPercentage = opts.showPercentage ~= false
  1461. o.text = opts.text
  1462. o.textSize = opts.textSize or opts.size
  1463. local dark = (current_theme == "dark")
  1464. o.backgroundColor = opts.backgroundColor or (dark and COLOR_GRAY or 0xC618)
  1465. o.progressColor = opts.progressColor or (dark and COLOR_BLUE or 0x001F)
  1466. o.borderColor = opts.borderColor or (dark and COLOR_WHITE or 0x8410)
  1467. o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
  1468. o.visible = opts.visible ~= false
  1469. o.enabled = opts.enabled ~= false
  1470. return o
  1471. end
  1472. function ProgressBar:setProgress(value)
  1473. self.progress = math.max(0, math.min(100, value))
  1474. end
  1475. function ProgressBar:getProgress()
  1476. return self.progress
  1477. end
  1478. function ProgressBar:setText(text)
  1479. self.text = text
  1480. end
  1481. function ProgressBar:draw()
  1482. if not self.visible then return end
  1483. -- 绘制背景(可选:只绘制内区,避免与边框重复像素)
  1484. local padding = 1
  1485. fill_rect(self.x + padding, self.y + padding, self.x + self.w - padding, self.y + self.h - padding, self.backgroundColor)
  1486. -- 绘制边框
  1487. stroke_rect(self.x, self.y, self.x + self.w, self.y + self.h, self.borderColor)
  1488. -- 计算进度条填充
  1489. padding = 1
  1490. local inner_left = self.x + padding
  1491. local inner_top = self.y + padding
  1492. local inner_right = self.x + self.w - padding -- 包含式
  1493. local inner_bottom = self.y + self.h - padding -- 包含式
  1494. local inner_width = inner_right - inner_left
  1495. local fill_width = math.floor(inner_width * (self.progress / 100))
  1496. if fill_width > 0 then
  1497. local x1 = inner_left
  1498. local x2 = inner_left + fill_width
  1499. fill_rect(x1, inner_top, x2, inner_bottom, self.progressColor)
  1500. end
  1501. -- 绘制文本
  1502. if self.showPercentage or self.text then
  1503. local display_text = self.text or (self.progress .. "%")
  1504. draw_text_in_rect_centered(self.x, self.y, self.w, self.h, display_text, {
  1505. color = self.textColor,
  1506. style = { size = self.textSize },
  1507. padding = 2
  1508. })
  1509. end
  1510. end
  1511. function ProgressBar:handleEvent(evt, x, y)
  1512. return false -- 进度条默认不处理触摸事件
  1513. end
  1514. -- ================================
  1515. -- 主模块导出
  1516. -- ================================
  1517. local M = {}
  1518. -- 核心API导出
  1519. M.init = core.init
  1520. M.add = core.add
  1521. M.remove = core.remove
  1522. M.clear = core.clear
  1523. M.render = core.render
  1524. M.handleTouchEvent = core.handleTouchEvent
  1525. M.debug = core.debug
  1526. M.getTheme = core.getTheme
  1527. -- 硬件支持
  1528. M.hw = hw
  1529. -- 事件系统
  1530. M.event = event
  1531. -- 组件构造函数
  1532. M.Button = function(opts) return Button:new(opts) end
  1533. M.CheckBox = function(opts) return CheckBox:new(opts) end
  1534. M.Label = function(opts) return Label:new(opts) end
  1535. M.Picture = function(opts) return Picture:new(opts) end
  1536. M.MessageBox = function(opts) return MessageBox:new(opts) end
  1537. M.Window = function(opts) return Window:new(opts) end
  1538. M.ProgressBar = function(opts) return ProgressBar:new(opts) end
  1539. -- 字体 API 导出
  1540. M.font = {
  1541. set = function(style) return font_set(style) end,
  1542. measure = function(text, style) return font_measure(text, style) end,
  1543. lineHeight = function(style) return font_line_height(style) end
  1544. }
  1545. -- 对外导出常用颜色常量,便于在业务侧直接使用
  1546. M.COLOR_WHITE = COLOR_WHITE
  1547. M.COLOR_BLACK = COLOR_BLACK
  1548. M.COLOR_GRAY = COLOR_GRAY
  1549. M.COLOR_BLUE = COLOR_BLUE
  1550. M.COLOR_RED = COLOR_RED
  1551. M.COLOR_GREEN = COLOR_GREEN
  1552. M.COLOR_YELLOW = COLOR_YELLOW
  1553. M.COLOR_CYAN = COLOR_CYAN
  1554. M.COLOR_MAGENTA = COLOR_MAGENTA
  1555. M.COLOR_ORANGE = COLOR_ORANGE
  1556. M.COLOR_PINK = COLOR_PINK
  1557. -- Windows 11 Light模式颜色
  1558. M.COLOR_WIN11_LIGHT_DIALOG_BG = COLOR_WIN11_LIGHT_DIALOG_BG
  1559. M.COLOR_WIN11_LIGHT_BUTTON_BG = COLOR_WIN11_LIGHT_BUTTON_BG
  1560. M.COLOR_WIN11_LIGHT_BUTTON_BORDER = COLOR_WIN11_LIGHT_BUTTON_BORDER
  1561. -- Windows 11 Dark模式颜色
  1562. M.COLOR_WIN11_DARK_DIALOG_BG = COLOR_WIN11_DARK_DIALOG_BG
  1563. M.COLOR_WIN11_DARK_BUTTON_BG = COLOR_WIN11_DARK_BUTTON_BG
  1564. M.COLOR_WIN11_DARK_BUTTON_BORDER = COLOR_WIN11_DARK_BUTTON_BORDER
  1565. return M