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