1--
2-- a pulldown terminal that lives outside the normal window manager
3-- down/upside is that input handling and other features behave differently
4--
5--
6-- MISSING:
7-- mouse/scroll-lock control, mouse injection, cut/paste(?), option to swap
8-- in other windows
9--
10
11local dstate = {
12	dir_x = 0,
13	dir_y = 1,
14	pos = "top",
15	ofs = 0,
16	width = 0.5,
17	height = 0.3,
18	shadow_color = {0xff, 0xff, 0xff}
19};
20
21gconfig_register("dt_width", 0.5);
22gconfig_register("dt_height", 0.4);
23gconfig_register("dt_opa", 0.8);
24gconfig_register("dt_shadow", 0);
25gconfig_register("dt_shadow_ofs_x", 0);
26gconfig_register("dt_shadow_ofs_y", 0);
27gconfig_register("dt_shadow_opa", 0.3);
28
29-- synch main font and fallback font with the registry
30local function set_font()
31	local tbl = {gconfig_get("term_font")};
32	local fbf = gconfig_get("font_fb");
33	if (not resource(tbl[1], SYS_FONT_RESOURCE)) then
34		return;
35	end
36
37	if (resource(fbf, SYS_FONT_RESOURCE)) then
38		tbl[2] = fbf;
39	end
40
41	if (valid_vid(dstate.term, TYPE_FRAMESERVER)) then
42		target_fonthint(dstate.term, tbl, gconfig_get("term_font_sz") * FONT_PT_SZ, -1);
43	end
44end
45
46local function set_sz()
47	if (valid_vid(dstate.term, TYPE_FRAMESERVER)) then
48		target_fonthint(dstate.term,
49			gconfig_get("term_font_sz") * FONT_PT_SZ, -1);
50	end
51end
52
53local function set_hint()
54	if (valid_vid(dstate.term, TYPE_FRAMESERVER)) then
55		target_fonthint(dstate.term, -1, gconfig_get("term_font_hint"));
56	end
57end
58
59local function update_shadow()
60	if (not valid_vid(dstate.term)) then
61		return;
62	end
63
64	if (valid_vid(dstate.shadow_vid)) then
65		delete_image(dstate.shadow_vid);
66	end
67
68	if (gconfig_get("dt_shadow") < 1) then
69		return;
70	end
71
72	local props = image_surface_resolve(dstate.term);
73	local shadow_sz = gconfig_get("dt_shadow");
74	dstate.shadow_vid = fill_surface(32, 32, unpack(dstate.shadow_color));
75	link_image(dstate.shadow_vid, dstate.term);
76	blend_image(dstate.shadow_vid, gconfig_get("dt_shadow_opa"));
77	order_image(dstate.shadow_vid, 65531);
78	resize_image(dstate.shadow_vid, props.width + shadow_sz, props.height + shadow_sz);
79	move_image(dstate.shadow_vid,
80		gconfig_get("dt_shadow_ofs_x"), gconfig_get("dt_shadow_ofs_y"));
81end
82
83gconfig_listen("term_font", "pdterm", set_font);
84gconfig_listen("term_font_sz", "pdterm_sz", set_sz);
85gconfig_listen("term_font_hint", "pdterm_hint", set_hint);
86
87-- it's so cheap so just rebuild etc. on everything
88gconfig_listen("dt_shadow", "pdterm_shadow", update_shadow);
89gconfig_listen("dt_shadow_opa", "pdterm_shadow_opa", update_shadow);
90gconfig_listen("dt_shadow_ofs_x", "pdterm_shadow_ofs_x", update_shadow);
91gconfig_listen("dt_shadow_ofs_y", "pdterm_shadow_ofs_y", update_shadow);
92
93local function update_size()
94	if (valid_vid(dstate.term, TYPE_FRAMESERVER)) then
95		local disp = active_display();
96		local neww = disp.width * gconfig_get("dt_width");
97		local newh = disp.height * gconfig_get("dt_height");
98		target_displayhint(dstate.term, neww, newh, 0, active_display().disptbl);
99		update_shadow();
100	end
101end
102
103local dterm_cfg = {
104{
105	label = "Width",
106	name = "width",
107	kind = "value",
108	hint = "(100 * val)%",
109	description = "Change the display relative window width",
110	validator = gen_valid_float(0.1, 1.0),
111	initial = function() return gconfig_get("dt_width"); end,
112	handler = function(ctx, val)
113		gconfig_set("dt_weight", tonumber(val));
114		update_size();
115	end
116},
117{
118	label = "Height",
119	name = "height",
120	kind = "value",
121	hint = "(100 * val)%",
122	description = "Change the display relative window height",
123	validator = gen_valid_float(0.1, 1.0),
124	initial = function() return gconfig_get("dt_width"); end,
125	handler = function(ctx, val)
126		gconfig_set("dt_height", tonumber(val));
127		update_size();
128	end
129},
130{
131	label = "Background Alpha",
132	name = "alpha",
133	kind = "value",
134	hint = "(0..1, 1=opaque)",
135	description = "Change the terminal background opacity",
136	validator = gen_valid_float(0.0, 1.0),
137	initial = function() return gconfig_get("dt_opa"); end,
138	handler = function(ctx, val)
139		gconfig_set("dt_opa", tonumber(val));
140		if (valid_vid(dstate.term, TYPE_FRAMESERVER)) then
141			target_graphmode(dstate.term, gconfig_get("dt_opa"));
142		end
143	end
144},
145{
146	name = "shadow",
147	label = "Shadow Size (px)",
148	kind = "value",
149	hint = "(0..100, 0 = disabled)",
150	description = "Set the size of the window hard-shadow box",
151	validator = gen_valid_float(0.0, 100.0),
152	initial = function() return gconfig_get("dt_shadow"); end,
153	handler = function(ctx, val)
154		gconfig_set("dt_shadow", tonumber(val));
155		gconfig_set("dt_shadow_ofs_x", -0.5 * val);
156		gconfig_set("dt_shadow_ofs_y", -0.5 * val);
157	end
158},
159{
160	name = "shadow_opa",
161	label = "Shadow Opacity",
162	kind = "value",
163	description = "Set the opacity of the window hard-shadow",
164	hint = "(0..1, 1=opaque)",
165	validator = gen_valid_float(0.0, 1.0),
166	initial = function() return gconfig_get("dt_shadow_opa"); end,
167	handler = function(ctx, val)
168		gconfig_set("dt_shadow_opa", tonumber(val));
169	end
170},
171{
172	name = "shadow_ofset_x",
173	label = "Shadow Ofset (X)",
174	kind = "value",
175	hint = "-50..50",
176	description = "Set the window pixel- X offset for the shadow",
177	validator = gen_valid_float(-50, 50),
178	initial = function() return gconfig_get("dt_shadow_ofs_x"); end,
179	handler = function(ctx, val)
180		gconfig_set("dt_shadow_ofs_x", tonumber(val));
181	end
182},
183{
184	name = "shadow_ofset_y",
185	label = "Shadow Ofset (Y)",
186	kind = "value",
187	hint = "-50..50",
188	description = "Set the window pixel- Y offset for the shadow",
189	validator = gen_valid_float(-50, 50),
190	initial = function() return gconfig_get("dt_shadow_ofs_y"); end,
191	handler = function(ctx, val)
192		gconfig_set("dt_shadow_ofs_y", tonumber(val));
193	end
194}
195};
196
197local atype = extevh_archetype("terminal");
198
199local function drop()
200	reset_image_transform(dstate.term);
201	local props = image_surface_properties(dstate.term);
202	nudge_image(dstate.term,
203		-1 * dstate.dir_x * props.width,
204		-1 * dstate.dir_y * props.height,
205		gconfig_get("animation")
206	);
207	blend_image(dstate.term, 0.0, gconfig_get("animation"));
208	dstate.active = false;
209	dispatch_toggle(false);
210	mouse_lockto(unpack(dstate.lock));
211
212	target_displayhint(dstate.term, 0, 0,
213		bit.bor(TD_HINT_UNFOCUSED, TD_HINT_INVISIBLE));
214end
215
216-- we intercept symbol- handling so our trigger path can be re-used
217-- but forward the rest to the new window
218local function ldisp(sym, iotbl, path)
219	if (not sym and not iotbl) then
220		drop();
221		return;
222	end
223
224-- label translation
225	if (iotbl.label and atype.labels[iotbl.label]) then
226		iotbl.label = atype.labels[iotbl.label];
227	end
228
229-- don't consume mouse here as we want to translate to the specific surface
230	if (iotbl.mouse) then
231		return false, sym, iotbl, path;
232	end
233
234	target_input(dstate.term, iotbl);
235
236-- run through the terminal archetype label binding
237	return true, sym, iotbl, path;
238end
239
240-- different rules apply to input and event response compared to normal windows
241-- we actually trust the terminal to be compliant here as it is launched
242-- authoritatively
243local function termh(source, status)
244	if (status.kind == "resized") then
245		resize_image(source, status.width, status.height);
246		update_shadow();
247	elseif (status.kind == "terminated") then
248		if (dstate.active) then
249			drop();
250		end
251		delete_image(source);
252		dstate.term = nil;
253		dstate.disp.lock_override = false;
254	end
255end
256
257local function dterm()
258-- if we're already toggled, then disable
259	if (dstate.active) then
260		drop();
261		return;
262	end
263
264-- if we got a terminal running, prefer that
265-- or spawn a new one.. create anchor, attach, order, ...
266	local disp = active_display();
267	local neww = disp.width * gconfig_get("dt_width");
268	local newh = disp.height * gconfig_get("dt_height");
269
270-- the spawn_terminal() function from global/open takes care of initial
271-- font setup, it's only for dynamic changes the rest is needed
272	if (not valid_vid(dstate.term)) then
273		local targ = terminal_build_argenv();
274		dstate.term = launch_avfeed(targ, "terminal",
275		function(source, status)
276			if (status.kind == "preroll") then
277				update_size();
278				set_font();
279				target_graphmode(dstate.term, gconfig_get("dt_opa"));
280				target_updatehandler(source, termh);
281			end
282		end
283		);
284		if (not valid_vid(dstate.term)) then
285			return;
286		end
287		dstate.disp = disp;
288		dstate.disp.lock_override = true;
289	end
290
291-- reattach to different output on switch or resize
292	if (dstate.disp ~= active_display()) then
293		dstate.disp.lock_override = false;
294		dstate.disp = active_display();
295		dstate.disp.lock_override = true;
296		target_displayhint(dstate.term, neww, newh, 0, dstate.disp.disptbl);
297		rendertarget_attach(active_display(true), dstate.term, RENDERTARGET_DETACH);
298		update_size();
299	end
300
301-- put us in the "special" overlay order range
302	order_image(dstate.term, 65532);
303
304-- center at hidden state non-dominant axis, account for user- padding
305-- do this the safe "all options rather than math" way due to the possible
306-- switching of user- config between spawns
307	if (dstate.dir_x ~= 0) then
308		neww = neww + dstate.ofs;
309		if (dstate.dir_x > 0) then
310			move_image(dstate.term, -neww, 0.5 * (disp.height - newh));
311			nudge_image(dstate.term, neww, 0, gconfig_get("animation"));
312		else
313			move_image(dstate.term, disp.width, 0.5 * (disp.height - newh));
314			nudge_image(dstate.term, -neww, 0, gconfig_get("animation"));
315		end
316	elseif (dstate.dir_y ~= 0) then
317		newh = newh + dstate.ofs;
318-- top
319		if (dstate.dir_y > 0) then
320			move_image(dstate.term, 0.5 * (disp.width - neww), -newh);
321			nudge_image(dstate.term, 0, newh, gconfig_get("animation"));
322 -- bottom
323		else
324			move_image(dstate.term, 0.5 * (disp.width - neww), disp.height);
325			nudge_image(dstate.term, 0, -newh, gconfig_get("animation"));
326		end
327	end
328
329	blend_image(dstate.term, 1.0, gconfig_get("animation"));
330	dstate.active = true;
331	dispatch_toggle(ldisp);
332
333-- since we don't have clipboard etc. mapping available we just toggle
334-- mouse input mode
335	target_input(dstate.term, {
336		kind = "digital", label = "MOUSE_FORWARD",
337		translated = true,
338		active = true,
339		devid = 8, subid = 8
340	});
341
342	local v, f, w, s = mouse_lockto(dstate.term,
343		function(rx, ry, x, y, state, ind, act)
344			local props = image_surface_properties(dstate.term);
345			x = x - props.x;
346			y = y - props.y;
347			if (ind) then
348				target_input(dstate.term, {
349					kind = "digital", mouse = true,
350					active = act, devid = 0, subid = ind
351				});
352			else
353				local iotbl = {
354				kind = "analog", mouse = true,
355				relative = false, devid = 0, subid = 0,
356				samples = {x}
357				};
358				target_input(dstate.term, iotbl);
359				iotbl.subid = 1;
360				iotbl.samples[1] = y;
361				target_input(dstate.term, iotbl);
362			end
363		end, nil
364	);
365	dstate.lock = {v, f, w, s};
366	target_displayhint(dstate.term, 0, 0, 0);
367end
368
369menus_register("global", "tools",
370{
371	name = "dterm",
372	label = "Drop-down Terminal",
373	kind = "action",
374	description = "A locked-input 'quake-style' terminal window",
375	handler = dterm
376});
377
378menus_register("global", "settings/tools",
379{
380	name = "dterm",
381	label = "Dropdown Terminal",
382	kind = "action",
383	submenu = true,
384	description = "Change how the dropdown terminal behaves",
385	handler = dterm_cfg
386});
387