1-- Copyright: 2015-2020, Björn Ståhl 2-- License: 3-Clause BSD 3-- Reference: http://durden.arcan-fe.com 4-- 5-- Description: lbar- is an input dialog- style bar intended for durden that 6-- supports some completion as well. It is somewhat messy as it grew without a 7-- real idea of what it was useful for then turned out to become really 8-- important. 9-- 10-- The big flawed design stem from all the hoops you have to go through to 11-- retain state after accepting/cancelling, and that it was basically designed 12-- to chain through itself (create -> [ok press] -> destroy [ok handler] -> 13-- call create again) etc. It worked OK when we didn't consider 14-- launch-for-binding, tooltip hints and meta up/down navigation 15-- but is now just ugly. 16-- 17-- the worst 'sin' is all the mixed/nested contexts, rough estimate: 18-- 19-- ictx = mapped to wm.input_ctx: 20-- wm matching active_display that may have an active lbar already (chain) 21-- 22-- ictx.inp = input state management for the text input, this uses the readline 23-- implementation from suppl.lua - but we first do our own input mgmt 24-- before forwarding 25-- 26-- ictx.cb_ctx = comp_ctx which is actually provided for the input callbacks 27-- 28-- where ictx.get_cb is the more important one here as it triggers the menu 29-- update but also when something has been input and selected correctly 30-- 31local function inp_str(ictx, valid) 32 local prefix = active_display():font_resfn(); 33 return { 34 prefix .. ( 35 valid and gconfig_get("lbar_textstr") or gconfig_get("lbar_alertstr")), 36 ictx.inp:view_str() 37 }; 38end 39 40local pending = {}; 41 42local function update_caret(ictx, mask) 43 local pos = ictx.inp.caretpos - ictx.inp.chofs; 44 if (pos == 0) then 45 move_image(ictx.caret, ictx.textofs, ictx.caret_y); 46 else 47 local msg = ictx.inp:caret_str(); 48 if (mask) then 49 msg = string.rep("*", string.len(msg)); 50 end 51 52 local prefix = active_display():font_resfn(); 53 local w, h = text_dimensions({prefix .. gconfig_get("lbar_textstr"), msg}); 54 move_image(ictx.caret, ictx.textofs+w, ictx.caret_y); 55 end 56end 57 58local active_lbar = nil; 59local function destroy(wm, ictx) 60 if (ictx.on_destroy) then 61 ictx:on_destroy(); 62 end 63 64 for i,v in ipairs(pending) do 65 mouse_droplistener(v); 66 end 67 pending = {}; 68 mouse_droplistener(ictx.bg_mh); 69 active_lbar = nil; 70 71-- if the statusbar is attached to the HUD (that we are on), reanchor it to the 72-- desktop so it doesn't get destroyed, then hide it so that it is not visible 73 if (gconfig_get("sbar_visible") == "hud") then 74 wm.statusbar:reanchor(wm.order_anchor, 2, wm.width, wm.statusbar.height); 75 wm.statusbar:hide(); 76 77-- if the statusbar is hidden entirely, force-hide it here again as the option 78-- might have changed in-flight 79 elseif (wm.hidden_sb) then 80 wm.statusbar:hide(); 81 else 82 83-- or make it visible again, the reason we hide it on hud activation is the 84-- blend-layer shining through and mixing with the widgets 85 wm.statusbar:show(); 86 end 87 88-- our lbar 89 local time = gconfig_get("transition"); 90 if (ictx.on_step and ictx.cb_ctx) then 91 ictx.on_step(ictx, -1); 92 end 93 94 blend_image(ictx.text_anchor, 0.0, time, INTERP_EXPOUT); 95 blend_image(ictx.anchor, 0.0, time, INTERP_EXPOUT); 96 97-- the 'time == 0' tend to be if animations are disabled entirely, or if 98-- we want to switch immediately to a different kind of bar - the use of 99-- PENDING_FADE here should be refactored entirely 100 if (time > 0) then 101 PENDING_FADE = ictx.anchor; 102 expire_image(ictx.anchor, time + 1); 103 tag_image_transform(ictx.anchor, MASK_OPACITY, function() 104 PENDING_FADE = nil; 105 end); 106 else 107 delete_image(ictx.anchor); 108 end 109 110-- now other components are free to grab input from the window manager 111 wm.input_ctx = nil; 112 wm:set_input_lock(); 113end 114 115local function accept_cancel(wm, accept, nofwd, m1) 116 local ictx = wm.input_ctx; 117 local inp = ictx.inp; 118 if (ictx.on_accept) then 119 ictx:on_accept(accept); 120 end 121 122 destroy(wm, ictx); 123 124-- the [nofwd] option is used to reset / trigger without causing 125-- the outer component to activate some cancellation action 126 if (not accept) then 127 if (ictx.on_cancel and not nofwd) then 128 ictx:on_cancel(m1); 129 end 130 return; 131 end 132 133 local base = inp.msg; 134 if (ictx.force_completion or string.len(base) == 0) then 135 if (inp.set and inp.set[inp.csel]) then 136 base = type(inp.set[inp.csel]) == "table" and 137 inp.set[inp.csel][3] or inp.set[inp.csel]; 138 end 139 end 140 141 ictx.get_cb(ictx.cb_ctx, base, true, inp.set, inp); 142end 143 144-- 145-- Build chain of single selectable strings, move and resize the marker to each 146-- of them, chain their positions to an anchor so they are easy to delete, and 147-- track an offset for drawing. We rebuild / redraw each cursor modification to 148-- ignore scrolling and tracking details. 149-- 150-- Set can contain the set of strings or a table of [colstr, selcolstr, text] 151-- This is incredibly wasteful in the sense that the list, cursor and handlers 152-- are reset and rebuilt- on every change. Should split out the cursor stepping 153-- and callback for "just change selection" scenario, and verify that set ~= 154-- last set. This dates back to the poor design of lbar/completion_cb. It's only 155-- saving grace is that 'n' is constrained by wm.width and label sizes so in 156-- 1..~20 or so- range. 157-- 158local function update_completion_set(wm, ctx, set) 159 if (not set) then 160 return; 161 end 162 ctx.ucount = ctx.ucount + 1; 163 local pad = gconfig_get("lbar_tpad") * wm.scalef; 164 if (ctx.canchor) then 165 delete_image(ctx.canchor); 166 for i,v in ipairs(pending) do 167 mouse_droplistener(v); 168 end 169 pending = {}; 170 ctx.canchor = nil; 171 ctx.citems = nil; 172 end 173 174-- track if set changes as we will need to reset 175 if (not ctx.inp.cofs or not ctx.inp.set or #set ~= #ctx.inp.set) then 176 ctx.inp.cofs = 1; 177 ctx.inp.csel = 1; 178 end 179 ctx.inp.set = set; 180 181 local on_step = wm.input_ctx.on_step; 182 local on_item = wm.input_ctx.on_item; 183 184-- clamp and account for paging 185 if (ctx.inp.clastc ~= nil and ctx.inp.csel < ctx.inp.cofs) then 186 local ocofs = ctx.inp.cofs; 187 ctx.inp.cofs = ctx.inp.cofs - ctx.inp.clastc; 188 ctx.inp.cofs = ctx.inp.cofs <= 0 and 1 or ctx.inp.cofs; 189 if (ocofs ~= ctx.inp.cofs and on_step) then 190 on_step(ctx); 191 end 192 end 193 194-- limitation with this solution is that we can't wrap around negative 195-- without forward stepping through due to variability in text length 196 ctx.inp.csel = ctx.inp.csel <= 0 and ctx.clim or ctx.inp.csel; 197 198-- wrap around if needed 199 if (ctx.inp.csel > #set) then 200 if (on_step and ctx.inp.cofs > 1) then on_step(ctx); end 201 ctx.inp.csel = 1; 202 ctx.inp.cofs = 1; 203 end 204 205-- very very messy positioning, relinking etc. can probably replace all 206-- this mess with just using uiprim_bar and buttons in center area 207 local regw = image_surface_properties(ctx.text_anchor).width; 208 local step = math.ceil(0.5 + regw / 3); 209 local ctxw = 2 * step; 210 local textw = valid_vid(ctx.text) and ( 211 image_surface_properties(ctx.text).width) or ctxw; 212 local lbarsz = math.ceil(gconfig_get("lbar_sz") * wm.scalef); 213 214 ctx.canchor = null_surface(wm.width, lbarsz); 215 image_tracetag(ctx.canchor, "lbar_anchor"); 216 217 move_image(ctx.canchor, step, 0); 218 if (not valid_vid(ctx.ccursor)) then 219 ctx.ccursor = color_surface(1, 1, unpack(gconfig_get("lbar_seltextbg"))); 220 image_tracetag(ctx.ccursor, "lbar_cursor"); 221 end 222 223 local ofs = 0; 224 local maxi = #set; 225 226 ctx.clim = #set; 227 228 local slide_window = function(i) 229 ctx.inp.clastc = i - ctx.inp.cofs; 230 ctx.inp.cofs = ctx.inp.csel; 231 if (on_step) then on_step(ctx); end 232 return update_completion_set(wm, ctx, set); 233 end 234 235 local sel_fmt = wm.font_delta .. gconfig_get("lbar_seltextstr"); 236 local txt_fmt = wm.font_delta .. gconfig_get("lbar_textstr"); 237 238 for i=ctx.inp.cofs,#set do 239-- figure out the format string and the message based on selection status 240-- and if the provided entry has a custom override or we should use def. 241 local selected = i == ctx.inp.csel; 242 local msgs; 243 244 if (type(set[i]) == "table") then 245 msgs = {wm.font_delta .. (set[i][selected and 2 or 1]), set[i][3]}; 246 else 247 msgs = {selected and sel_fmt or txt_fmt, set[i]}; 248 end 249 250 local w, h = text_dimensions(msgs); 251 local exit = false; 252 local crop = false; 253 254-- special case, w is too large to fit, just crop to acceptable length 255-- maybe improve this by adding support for a shortening, have full-name 256-- in some cursor relative hint 257 if (w > 0.3 * ctxw) then 258 w = math.floor(0.3 * ctxw); 259 crop = true; 260 end 261 262-- outside display? show ->, if that's our index, slide page. Tacitly assume 263-- that the normal arrow glyphs are indeed present in the way that even the 264-- builtin- pixel font accepts. 265 if (i ~= ctx.inp.cofs and ofs + w > ctxw - 10) then 266 msgs = {txt_fmt, gconfig_get("lbar_nextsym")}; 267 exit = true; 268 ctx.last_cell = i 269 270 if (i == ctx.inp.csel) then 271 return slide_window(i); 272 end 273 end 274 275-- render, attach, position, order 276 local txt, lines, txt_w, txt_h, asc = render_text(msgs); 277 278 image_tracetag(txt, "lbar_text" ..tostring(i)); 279 link_image(ctx.canchor, ctx.text_anchor); 280 link_image(txt, ctx.canchor); 281 link_image(ctx.ccursor, ctx.canchor); 282 image_inherit_order(ctx.canchor, true); 283 image_inherit_order(ctx.ccursor, true); 284 image_inherit_order(txt, true); 285 order_image(txt, 2); 286 image_clip_on(txt, CLIP_SHALLOW); 287 order_image(ctx.ccursor, 1); 288 289-- try to avoid very long items from overflowing their slot, 290-- should "pop up" a copy when selected instead where the full 291-- name is shown as part of the description helper 292 if (crop) then 293 crop_image(txt, w, h); 294 end 295 296-- allow (but sneer!) mouse for selection and activation, missing 297-- an entry to handle "last-page back to first" though 298 local mh = { 299 name = "lbar_labelsel", 300 own = function(mctx, vid) 301 return vid == txt or vid == mctx.child; 302 end, 303 motion = function(mctx) 304 if (ctx.inp.csel == i) then 305 return; 306 end 307 if (on_step) then 308 on_step(ctx, i, msgs[2], ctx.text_anchor, 309 mctx.mofs + mctx.mstep, mctx.mwidth, mctx); 310 end 311 ctx.inp.csel = i; 312 resize_image(ctx.ccursor, w, lbarsz); 313 move_image(ctx.ccursor, mctx.mofs, 0); 314 end, 315 click = function() 316 if (exit) then 317 return slide_window(i); 318 else 319 accept_cancel(wm, true); 320 end 321 end, 322-- need copies of these into returned context for motion handler 323 mofs = ofs, 324 mstep = step, 325 mwidth = w 326 }; 327 328 mouse_addlistener(mh, {"motion", "click"}); 329 table.insert(pending, mh); 330 show_image({txt, ctx.ccursor, ctx.canchor}); 331 332-- update cursor for the selected item, and optionally forward to a 333-- caller provided hook (from context setup) 334 if (selected) then 335 move_image(ctx.ccursor, ofs, 0); 336 resize_image(ctx.ccursor, w, lbarsz); 337 if (on_step) then 338 on_step(ctx, i, msgs[2], ctx.text_anchor, ofs + step, w, mh); 339 end 340 end 341 342-- forward setup so that content relative helpers can be pruned / updated 343 if (on_item) then 344 on_item( 345 ctx, i, msgs[2], selected, ctx.text_anchor, ofs + step, w, 346 exit or i == #set 347 ); 348 end 349 350 move_image(txt, ofs, pad); 351 ofs = ofs + (crop and w or txt_w) + gconfig_get("lbar_itemspace"); 352-- can't fit more entries, give up 353 if (exit) then 354 ctx.clim = i-1; 355 break; 356 end 357 end 358end 359 360local function setup_string(wm, ictx, str) 361 local tvid, heights, textw, texth = render_text(str); 362 if (not valid_vid(tvid)) then 363 return ictx; 364 end 365 366 local pad = gconfig_get("lbar_tpad") * wm.scalef; 367 368 ictx.text = tvid; 369 image_tracetag(ictx.text, "lbar_inpstr"); 370 show_image(ictx.text); 371 link_image(ictx.text, ictx.text_anchor); 372 image_inherit_order(ictx.text, true); 373 374 move_image(ictx.text, ictx.textofs, pad); 375 376 return tvid; 377end 378 379local function lbar_istr(wm, ictx, res) 380-- other option would be to run ictx.inp:undo, which was the approach earlier, 381-- but that prevented the input of more complex values that could go between 382-- valid and invalid. Now we just visually indicate. 383 local str = inp_str(ictx, not (res == false or res == nil)); 384 if (ictx.mask_text) then 385 str[2] = string.rep("*", string.len(str[2])); 386 end 387 388 if (valid_vid(ictx.text)) then 389 ictx.text = render_text(ictx.text, str); 390 else 391 ictx.text = setup_string(wm, ictx, str); 392 end 393 394 update_caret(ictx, ictx.mask_text); 395end 396 397local function lbar_ih(wm, ictx, inp, sym, caret) 398 if (caret ~= nil) then 399 update_caret(ictx, ictx.mask_text); 400 return; 401 end 402 inp.csel = inp.csel and inp.csel or 1; 403 local res = ictx.get_cb(ictx.cb_ctx, ictx.inp.msg, false, ictx.inp.set, ictx.inp); 404 405-- special case, we have a strict set to chose from 406 if (type(res) == "table" and res.set) then 407 update_completion_set(wm, ictx, res.set); 408 end 409 410 lbar_istr(wm, ictx, res); 411end 412 413local function lbar_readline_input(wm, ictx, sym, m1) 414-- ctrl+p to up 415 if sym == "p" or sym == "UP" then 416 sym = ictx.cancel 417 return true, sym, true 418 end 419 420-- ctrl+a to home 421 if sym == "a" then 422 ictx.inp:caret_home() 423 return 424 end 425 426-- ctrl+e to end 427 if sym == "e" then 428 ictx.inp:caret_end() 429 return 430 end 431 432-- ctrl+l to clear 433 if sym == "l" then 434 ictx.inp:clear() 435 update_completion_set(wm, ictx, ictx.inp.set) 436 return 437 end 438 439-- try to step a page forward 440 if sym == "k" then 441 ictx.inp.csel = ictx.inp.csel + 2 442 if ictx.last_cell then 443 ictx.inp.csel = ictx.last_cell 444 ictx.inp.clastc = ictx.last_cell - ictx.inp.cofs 445 update_completion_set(wm, ictx, ictx.inp.set) 446 end 447 return 448 end 449 450 return false, sym, m1 451end 452 453-- used on spawn to get rid of crossfade effect 454PENDING_FADE = nil; 455function lbar_input(wm, sym, iotbl, lutsym, meta) 456 local ictx = wm.input_ctx; 457 local m1, m2 = dispatch_meta(); 458 459-- some old wm:input_lock handler, just ignore here 460 if (meta) then 461 return; 462 end 463 464-- and only trigger on rising edge 465 if (not iotbl.active) then 466 return; 467 end 468 469-- also remap control to modifier for readline-esque behaviors 470 if iotbl.modifiers and iotbl.modifiers > 0 then 471 local ms = decode_modifiers(iotbl.modifiers, "_") 472 473 if ms == "lctrl" or ms == "rctrl" then 474 local cont 475 cont, sym, m1 = lbar_readline_input(wm, ictx, sym, m1) 476 if not cont then 477 return 478 end 479 end 480 end 481 482-- first allow whatever thing that is using the lbar to override the 483-- meta + accept/l/r/n/p in order to implement more advanced actions 484-- 485-- this was dropped from not being used and making certain operations even more 486-- complicated, primary one being page left/right 487-- 488--if (m1 and (sym == ictx.cancel or sym == ictx.accept or 489-- sym == ictx.caret_left or sym == ictx.caret_right or 490-- sym == ictx.step_n or sym == ictx.step_p)) then 491-- if (ictx.meta_handler and ictx:meta_handler(sym, iotbl, lutsym, meta)) then 492-- return 493-- end 494-- end 495 496-- meta held means commit and relaunch at our current position, 497-- this is problematic due to a. caching and b. path mutating 498 if (sym == ictx.cancel or sym == ictx.accept) then 499 return accept_cancel(wm, sym == ictx.accept, false, m1); 500 end 501 502 if ((sym == ictx.step_n or sym == ictx.step_p)) then 503 if (ictx.inp and ictx.inp.csel) then 504 ictx.inp.csel = (sym == ictx.step_n) and 505 (ictx.inp.csel+1) or (ictx.inp.csel-1); 506 end 507 update_completion_set(wm, ictx, ictx.inp.set); 508 return; 509 end 510 511-- special handling, if the user hasn't typed anything, map caret 512-- manipulation to completion navigation as well) 513 if (ictx.inp and ictx.inp.csel) then 514 local upd = false; 515 if (ictx.invalid) then 516 upd = true; 517 ictx.invalid = false; 518 end 519 520 if (string.len(ictx.inp.msg) < ictx.inp.caretpos and 521 sym == ictx.caret_right) then 522 ictx.inp.csel = ictx.inp.csel + 1; 523 upd = true; 524 525 elseif (ictx.inp.caretpos == 1 and ictx.inp.chofs == 1 and 526 sym == ictx.caret_left) then 527 ictx.inp.csel = ictx.inp.csel - 1; 528 upd = true; 529 end 530 ictx.invalid = false; 531 if (upd) then 532 update_completion_set(wm, ictx, ictx.inp.set); 533 return; 534 end 535 end 536 537-- note, inp ulim can be used to force a sliding view window, not 538-- useful here but still implemented. 539 local keys = { 540 k_left = SYSTEM_KEYS["left"], 541 k_right = SYSTEM_KEYS["right"], 542 k_home = SYSTEM_KEYS["home"], 543 k_end = SYSTEM_KEYS["end"], 544 k_delete = SYSTEM_KEYS["delete"], 545 k_erase = SYSTEM_KEYS["erase"] 546 }; 547 548-- forward to the read/edit-line like tool 549 ictx.inp = suppl_text_input( 550 ictx.inp, iotbl, sym, 551 function(inp, sym, caret) 552 lbar_ih(wm, ictx, inp, sym, caret); 553 end, 554 { 555 bindings = keys 556 } 557 ); 558 ictx.ucount = 0; 559 ictx.ulim = 10; 560 561-- unfortunately the haphazard lbar design makes filtering / forced reverting 562-- to a previous state a bit clunky, get_cb -> nil? nothing, -> false? don't 563-- permit, -> tbl with set? change completion view 564 local res = ictx.get_cb(ictx.cb_ctx, ictx.inp.msg, false, ictx.inp.set, ictx.inp); 565 if (res == false) then 566-- ictx.inp:undo(); 567 elseif (res == true) then 568 elseif (res ~= nil and res.set) then 569 update_completion_set(wm, ictx, res.set); 570 end 571end 572 573local function lbar_helper(lbar, lbl) 574 local wm = active_display(); 575 local barh = math.ceil(gconfig_get("lbar_sz") * wm.scalef); 576 577 local dst; 578 if (type(lbl) == "table") then 579 dst = lbl; 580 else 581 if (not lbl or string.len(lbl) == 0) then 582 if (valid_vid(lbar.helper_bg)) then 583 hide_image(lbar.helper_bg); 584 end 585 return; 586 end 587 dst = {wm.font_delta .. gconfig_get("lbar_helperstr"), lbl}; 588 end 589 590-- don't repeat ourselves 591 if (lbl == lbar.last_helper) then 592 return; 593 end 594 lbar.last_helper = lbl; 595 596-- build text and bar 597 local pad = gconfig_get("lbar_tpad") * wm.scalef; 598 if (not lbar.helper_bg) then 599 lbar.helper_bg = color_surface(64, barh, unpack(gconfig_get("lbar_helperbg"))); 600 shader_setup(lbar.helper_bg, "ui", "rounded"); 601 image_inherit_order(lbar.helper_bg, true); 602 link_image(lbar.helper_bg, lbar.text_anchor); 603 show_image(lbar.helper_bg); 604 local w; 605 lbar.helper_lbl, _, w = render_text(dst); 606 image_inherit_order(lbar.helper_lbl, true); 607 link_image(lbar.helper_lbl, lbar.helper_bg); 608 show_image(lbar.helper_lbl); 609 move_image(lbar.helper_lbl, 2, pad); 610 nudge_image(lbar.helper_bg, 0, -barh); 611 resize_image(lbar.helper_bg, w + 4, barh); 612 613-- just re-render text and show bar 614 else 615 local w; 616 show_image(lbar.helper_bg); 617 _, _, w = render_text(lbar.helper_lbl, dst); 618 move_image(lbar.helper_lbl, 2, pad); 619 resize_image(lbar.helper_bg, w + 4, barh); 620 end 621end 622 623local function lbar_label(lbar, lbl) 624 if (valid_vid(lbar.labelid)) then 625 delete_image(lbar.labelid); 626 if (lbl == nil) then 627 lbar.textofs = 0; 628 return; 629 end 630 end 631 632 local wm = active_display(); 633 634 local id, lines, w, h, asc = render_text({wm.font_delta .. 635 gconfig_get("lbar_labelstr"), lbl}); 636 637 lbar.labelid = id; 638 if (not valid_vid(lbar.labelid)) then 639 return; 640 end 641 642 image_tracetag(id, "lbar_labelstr"); 643 show_image(id); 644 link_image(id, lbar.text_anchor); 645 image_inherit_order(id, true); 646 order_image(id, 1); 647 648 local pad = gconfig_get("lbar_tpad") * wm.scalef; 649-- relinking / delinking on changes every time 650 move_image(lbar.labelid, pad, pad); 651 lbar.textofs = w + gconfig_get("lbar_spacing") * wm.scalef; 652 653 if (valid_vid(lbar.text)) then 654 move_image(lbar.text, lbar.textofs, pad); 655 end 656 update_caret(lbar); 657end 658 659-- construct a default lbar callback that triggers cb on an exact 660-- content match of the tbl- table 661function tiler_lbarforce(tbl, cb) 662 return function(ctx, instr, done, last) 663 if (done) then 664 cb(instr); 665 return; 666 end 667 668 if (instr == nil or string.len(instr) == 0) then 669 return {set = tbl, valid = true}; 670 end 671 672 local res = {}; 673 for i,v in ipairs(tbl) do 674 if (string.sub(v,1,string.len(instr)) == instr) then 675 table.insert(res, v); 676 end 677 end 678 679-- want to return last result table so cursor isn't reset 680 if (last and #res == #last) then 681 return {set = last}; 682 end 683 684 return {set = res, valid = true}; 685 end 686end 687 688function tiler_lbar_isactive(ref) 689 if (ref) then 690 return active_lbar; 691 else 692 return active_lbar ~= nil; 693 end 694end 695 696function tiler_lbar_setactive(slot) 697 if (active_lbar) then 698 active_lbar:destroy() 699 end 700 701 active_lbar = slot; 702end 703 704local function lbar_destroy(lbar, nofwd) 705 accept_cancel(lbar.wm, false, nofwd); 706end 707 708function tiler_lbar(wm, completion, comp_ctx, opts) 709 opts = opts == nil and {} or opts; 710 local time = gconfig_get("transition"); 711 712-- hack around animation-out when something suddenly triggers a new lbar 713-- during an ongoing fade' 714 if (valid_vid(PENDING_FADE)) then 715 delete_image(PENDING_FADE); 716 time = 0; 717 end 718 719 PENDING_FADE = nil; 720 if (active_lbar) then 721 warning("tried to spawn multiple lbars"); 722 active_lbar:destroy(); 723 end 724 725-- the bg is mainly input capture 726 local bg = color_surface(wm.width, wm.height, 255, 0, 0); 727 image_tracetag(bg, "lbar_bg"); 728 shader_setup(bg, "ui", "lbarbg"); 729 image_tracetag(bg, "lbar_bg"); 730 731--actual completion bar, gconfig- controled base color 732 local barh = math.ceil(gconfig_get("lbar_sz") * wm.scalef); 733 local bar = color_surface(wm.width, barh, unpack(gconfig_get("lbar_bg"))); 734 shader_setup(bar, "ui", "lbar"); 735 image_tracetag(bar, "lbar_text"); 736 737-- the wm order anchor is a null surface expected to be in the Z order above 738-- the last visibile desktop item 739 link_image(bg, wm.order_anchor); 740 link_image(bar, bg); 741 image_inherit_order(bar, true); 742 image_inherit_order(bg, true); 743 image_mask_clear(bar, MASK_OPACITY); 744 745 blend_image(bg, gconfig_get("lbar_dim"), time, INTERP_EXPOUT); 746 order_image(bg, 1); 747 order_image(bar, 3); 748 blend_image(bar, 1.0, time, INTERP_EXPOUT); 749 750-- caret, size in pixels and scaled based on relative to base- density - 751-- when we get a surface based on tui- instead this could be moved to simply 752-- using the cursor attribute and move that around 753 local car = color_surface( 754 wm.scalef * gconfig_get("lbar_caret_w"), 755 wm.scalef * gconfig_get("lbar_caret_h"), 756 unpack(gconfig_get("lbar_caret_col")) 757 ); 758 show_image(car); 759 image_inherit_order(car, true); 760 link_image(car, bar); 761 local carety = gconfig_get("lbar_tpad") * wm.scalef; 762 763 move_image(bar, 0, math.floor(0.5*(wm.height-barh))); 764 765-- grab all input that gets routed through the WM 766 wm:set_input_lock(lbar_input, "bbar"); 767 768 local res = { 769 anchor = bg, 770 text_anchor = bar, 771 mask_text = opts.password_mask, 772 773-- we cache these per context as we don't want them changing mid- use, 774-- which can practically happen if binding is activated, even though suppl_input 775-- also tracks these bindings, we want them here for the overloaded navigation 776-- we have based on caret realative state 777 accept = SYSTEM_KEYS["accept"], 778 cancel = SYSTEM_KEYS["cancel"], 779 step_n = SYSTEM_KEYS["next"], 780 step_p = SYSTEM_KEYS["previous"], 781 caret_left = SYSTEM_KEYS["left"], 782 caret_right = SYSTEM_KEYS["right"], 783 784 textstr = gconfig_get("lbar_textstr"), 785 set_label = lbar_label, 786 set_helper = lbar_helper, 787 get_cb = completion, 788 cb_ctx = comp_ctx, 789 destroy = lbar_destroy, 790 791-- own caret tracking, should probably just be moved to using suppl_input 792 cofs = 1, 793 csel = 1, 794 ucount = 0, 795 barh = barh, 796 textofs = 0, 797 caret = car, 798 caret_y = carety, 799 800-- hooks for implementing separate behavior, biggest use is file- browser like 801-- behavior where previews need to be loaded, cached and aligne based on actual 802-- item state 803 on_step = opts.on_step, 804 on_destroy = opts.on_destroy, 805 on_item = opts.on_item, 806 in_preview = opts.in_preview, 807 on_accept = opts.on_accept, 808 on_create = opts.on_create, 809 on_entry = opts.on_entry, 810 wm = wm, 811 }; 812 813-- if not set, default to true, determines if results should be forwarded to the 814-- caller as they are typed or based on the selected item (if any) 815 if (opts.force_completion == false) then 816 res.force_completion = false; 817 else 818 res.force_completion = true; 819 end 820 821 wm.input_ctx = res; 822 823 local bg_mh = { 824 name = "bg_cancel", 825 own = function(ctx, vid) return vid == bg; end, 826 click = function() 827 accept_cancel(wm, true); 828 end, 829 rclick = function() 830 accept_cancel(wm, false); 831 end, 832 button = function(ctx, vid, ind, act) 833 if (not act or ind > MOUSE_WHEELNX or ind < MOUSE_WHEELPY) then 834 return; 835 end 836 local d = (ind == MOUSE_WHEELNX or ind == MOUSE_WHEELNY) and 1 or -1; 837 if (res.inp and res.inp.csel) then 838 res.inp.csel = res.inp.csel + d; 839 update_completion_set(wm, res, res.inp.set); 840 end 841 end 842 } 843 mouse_addlistener(bg_mh, {"click", "rclick", "button"}); 844 res.bg_mh = bg_mh; 845 846-- arbitrary overrides hack, see menu.lua but used for previews 847 if (opts.overlay) then 848 for k,v in pairs(opts.overlay) do 849 res[k] = v; 850 end 851 end 852 853-- restore from previous population / selection 854 if (opts.restore and opts.restore.msg) then 855 if (string.len(opts.restore.msg) > 1 and 856 opts.restore.cofs == 1 and opts.restore.csel == 1) then 857-- we treat this case as new as it left with many prefix+1 res that had to be 858-- erased to get to the set the user actually wanted 859 else 860 res.inp = opts.restore; 861 res.invalid = true; 862 end 863 end 864 865 if (res.on_create) then 866 res:on_create(opts.restore); 867 end 868 869-- send a fake, empty keypress to seed input state 870 lbar_input(wm, "", { 871 active = true, 872 kind = "digital", 873 translated = true, 874 devid = 0, subid = 0 875 }); 876 lbar_istr(wm, res, true); 877 878-- don't want this one running here as there might be actions bound that 879-- alter bar state, breaking synch between data model and user 880 if (gconfig_get("sbar_visible") == "hud") then 881 wm.statusbar:show(); 882 move_image(wm.statusbar.anchor, 0, gconfig_get("sbar_pos") == "top" 883 and 0 or wm.height - image_surface_resolve(wm.statusbar.anchor).height); 884 else 885 wm.statusbar:hide(); 886 end 887 888-- label is suggested context indicator, used for hint in value input 889 if (opts.label) then 890 res:set_label(opts.label); 891 end 892 893 active_lbar = res; 894 return res; 895end 896