1 /**
2 	GUI.c
3 	This file contains functions that are used for layouting custom menus.
4 
5 	@author Zapper
6 */
7 
8 /* -- constants used for layout -- */
9 
10 static const GUI_AlignLeft = -1;
11 static const GUI_AlignCenter = 0;
12 static const GUI_AlignRight = +1;
13 
14 static const GUI_AlignTop = -1;
15 static const GUI_AlignBottom = +1;
16 
17 /*
18  Prototype for the alternative layout:
19 
20  Propterties:
21  - Align: proplist, defines the alignment via alignment constnats GUI_Align*
22           - X: Horizontal alignment: Left, Center, Right
23           - Y: Vertical alginment: Top, Center, Bottom
24  - Margin: proplist, defines the margin of the GUI element
25            - Left: Margin on the left side of the element
26            - Right: Margin on the right side of the element
27            - Top: Margin on top of the element
28            - Bottom: Margin on the bottom of the element
29            Note: The margin dimension is defined by the property "Dimension"
30  - Width: int, width of the element
31  - Height: int, height of the element
32  - Dimension: function, defines the unit for the properties "Margin", "Width", and "Height";
33               Should be one of: Global.ToPercentString (default), or Global.ToEmString
34  */
35 static const GUI_BoxLayout = new Global {
36 	Align = { X = GUI_AlignLeft, Y = GUI_AlignTop,},
37 	Margin = { Left = 0, Right = 0, Top = 0, Bottom = 0,},
38 	Width = 0,
39 	Height = 0,
40 	Dimension = Global.ToPercentString,
41 };
42 
43 /*
44  Prototype for grid layout:
45 
46  Properties:
47  - Grid: proplist, as GUI_BoxLayout; additional properties:
48          - Rows (default = 1): The number of rows in the grid
49          - Columns (default = 1): The nnumber of columns in the grid
50          Defines the layout for the box that contains the grid
51  - Cell: proplist, as GUI_BoxLayout
52          Defines the layout for individual cells in the grid
53 
54  */
55 static const GUI_GridCellLayout = new Global {
56 	Grid = { Prototype = GUI_BoxLayout, Rows = 1, Columns = 1},
57 	Cell = { Prototype = GUI_BoxLayout, },
58 };
59 
60 
61 /* -- menu functions -- */
62 
63 // documented in /docs/sdk/script/fn
GuiAction_Call(proplist target,string function,value)64 global func GuiAction_Call(proplist target, string function, value)
65 {
66 	return [GUI_Call, target, function, value];
67 }
68 
69 // documented in /docs/sdk/script/fn
GuiAction_SetTag(string tag,int subwindow,object target)70 global func GuiAction_SetTag(string tag, int subwindow, object target)
71 {
72 	return [GUI_SetTag, tag, subwindow, target];
73 }
74 
GuiAddCloseButton(proplist menu,proplist target,string callback,parameter)75 global func GuiAddCloseButton(proplist menu, proplist target, string callback, parameter)
76 {
77 	var close_button =
78 	{
79 		Tooltip = "$TooltipGUIClose$",
80 		Priority = 0x0fffff,
81 		Left = "100%-2em", Top = "0%+0em",
82 		Right = "100%", Bottom = "0%+2em",
83 		Symbol = GetDefaultCancelSymbol(),
84 		BackgroundColor = {Std = 0, Hover = 0x50ffff00},
85 		OnMouseIn = GuiAction_SetTag("Hover"),
86 		OnMouseOut = GuiAction_SetTag("Std"),
87 		OnClick = GuiAction_Call(target, callback, parameter)
88 	};
89 	GuiAddSubwindow(close_button, menu);
90 	return close_button;
91 }
92 
GuiUpdateText(string text,int menu,int submenu,object target)93 global func GuiUpdateText(string text, int menu, int submenu, object target)
94 {
95 	var update = {Text = text};
96 	GuiUpdate(update, menu, submenu, target);
97 	return true;
98 }
99 
100 // adds proplist /submenu/ as a new property to /menu/
GuiAddSubwindow(proplist submenu,proplist menu)101 global func GuiAddSubwindow(proplist submenu, proplist menu)
102 {
103 	do
104 	{
105 		// use an anonymous name starting with an underscore
106 		var uniqueID = Format("_child%d", RandomX(10000, 0xffffff));
107 		if (menu[uniqueID] != nil) continue;
108 		menu[uniqueID] = submenu;
109 		return true;
110 	} while (true);
111 }
112 
113 // Converts an integer into a floating "em"-value, as the given value is divided by the given factor (10 by default).
ToEmString(int value,int factor)114 global func ToEmString(int value, int factor)
115 {
116 	// Make sure factor is a power of ten.
117 	factor = factor ?? 10;
118 	var power_of_ten = 0;
119 	while (10**power_of_ten != factor)
120 	{
121 		if (10**power_of_ten > factor)
122 		{
123 			Log("WARNING: factor in ToEmString(%d, %d) is not a multiple of ten, falling back to default", value, factor);
124 			factor = 10;
125 			power_of_ten = 1;
126 			break;
127 		}
128 		power_of_ten++;
129 	}
130 	// Construct the string using sign, value and decimal notation.
131 	var em_sign = "+";
132 	if (value < 0)
133 		em_sign = "-";
134 	var em_value = Format("%d", Abs(value / factor));
135 	var em_decimal = Format("%011dem", Abs(value % factor));
136 	em_decimal = TakeString(em_decimal, GetLength(em_decimal) - power_of_ten - 2);
137 	if (power_of_ten == 0)
138 		em_decimal = "0";
139 	return Format("%s%s.%s", em_sign, em_value, em_decimal);
140 }
141 
142 // Converts an integer into a floating percent value, as the given value is divided by the given factor (10 by default).
ToPercentString(int value,int factor)143 global func ToPercentString(int value, int factor)
144 {
145 	// Make sure factor is a power of ten.
146 	factor = factor ?? 10;
147 	var power_of_ten = 0;
148 	while (10**power_of_ten != factor)
149 	{
150 		if (10**power_of_ten > factor)
151 		{
152 			Log("WARNING: factor in ToPercentString(%d, %d) is not a multiple of ten, falling back to default", value, factor);
153 			factor = 10;
154 			power_of_ten = 1;
155 			break;
156 		}
157 		power_of_ten++;
158 	}
159 	// Construct the string using sign, value and decimal notation.
160 	var percent_sign = "+";
161 	if (value < 0)
162 		percent_sign = "-";
163 	var percent_value = Format("%d", Abs(value / factor));
164 	var percent_decimal = Format("%011d%%", Abs(value % factor));
165 	percent_decimal = TakeString(percent_decimal, GetLength(percent_decimal) - power_of_ten - 1);
166 	if (power_of_ten == 0)
167 		percent_decimal = "0";
168 	return Format("%s%s.%s", percent_sign, percent_value, percent_decimal);
169 }
170 
171 /*
172 Returns true if /this/ object is allowed to be displayed on the same stack as the /other/ object in a GUI.
173 */
CanBeStackedWith(object other)174 global func CanBeStackedWith(object other)
175 {
176 	return this->GetID() == other->GetID();
177 }
178 
179 // Returns the default symbol used for the "cancel" icon displayed e.g. in the top-right corner of menus.
GetDefaultCancelSymbol()180 global func GetDefaultCancelSymbol()
181 {
182 	return _inherited(...);
183 }
184 
185 /* -- layout functions -- */
186 
187 /*
188  Checks a layout for errors, and throw a fatal error if the layout is wrong.
189 
190  @par layout A proplist with prototype GUI_BoxLayout.
191              It is checked that:
192              - Width is not 0
193              - Height is not 0
194              - Dimension is Global.ToPercentString or Global.ToEmString
195  */
GuiCheckLayout(proplist layout)196 global func GuiCheckLayout(proplist layout)
197 {
198 	var errors = [];
199 	if (layout.Width == 0)
200 	{
201 		PushBack(errors, "property 'Width' must not be 0");
202 	}
203 	if (layout.Height == 0)
204 	{
205 		PushBack(errors, "property 'Height' must not be 0");
206 	}
207 	if (layout.Dimension != Global.ToEmString && layout.Dimension != Global.ToPercentString)
208 	{
209 		PushBack(errors, Format("property 'Dimension' must be Global.ToEmString, or Global.ToPerccentString, but it is %v", layout.Dimension));
210 	}
211 
212 	if (GetLength(errors) > 0)
213 	{
214 		var message = "Error in layout";
215 		for (var error in errors)
216 		{
217 			message = Format("%s, %s", message, error);
218 		}
219 		FatalError(message);
220 	}
221 }
222 
223 
224 /*
225  Calculates the position for a box element.
226 
227  This function returns a proplist with the properties: Left, Right, Top, Bottom.
228  The proplist can be added to a GUI proplist in order to define the position
229  of said GUI element - simply merge with AddProperties(element, position);
230 
231  @par layout A proplist with prototype GUI_BoxLayout;
232  */
GuiCalculateBoxElementPosition(proplist layout)233 global func GuiCalculateBoxElementPosition(proplist layout)
234 {
235 	GuiCheckLayout(layout);
236 
237 	var element_width = layout.Width + layout.Margin.Left + layout.Margin.Right;
238 	var element_height = layout.Height + layout.Margin.Top + layout.Margin.Bottom;
239 
240 	// determine alignment on x axis
241 	var align_x;
242 	var offset_x;
243 	if (layout.Align.X == GUI_AlignLeft)
244 	{
245 		align_x = "0%";
246 		offset_x = 0;
247 	}
248 	else if (layout.Align.X == GUI_AlignCenter)
249 	{
250 		align_x = "50%";
251 		offset_x = -element_width / 2;
252 	}
253 	else if (layout.Align.X == GUI_AlignRight)
254 	{
255 		align_x = "100%";
256 		offset_x = -element_width;
257 	}
258 
259 	// determine alignment on y axis
260 	var align_y;
261 	var offset_y;
262 	if (layout.Align.Y == GUI_AlignTop)
263 	{
264 		align_y = "0%";
265 		offset_y = 0;
266 	}
267 	else if (layout.Align.Y == GUI_AlignCenter)
268 	{
269 		align_y = "50%";
270 		offset_y = -element_height / 2;
271 	}
272 	else if (layout.Align.Y == GUI_AlignBottom)
273 	{
274 		align_y = "100%";
275 		offset_y = -element_height;
276 	}
277 
278 	// determine actual dimensions
279 
280 	var element_x = offset_x + layout.Margin.Left;
281 	var element_y = offset_y + layout.Margin.Top;
282 
283 	return
284 	{
285 		Left =   Format("%s%s", align_x, Call(layout.Dimension, element_x)),
286 		Top =    Format("%s%s", align_y, Call(layout.Dimension, element_y)),
287 		Right =  Format("%s%s", align_x, Call(layout.Dimension, element_x + layout.Width)),
288 		Bottom = Format("%s%s", align_y, Call(layout.Dimension, element_y + layout.Height))
289 	};
290 }
291 
292 
293 /*
294  Calculates the position for a box element inside a grid of elements.
295  For example, the buttons in the inventory bar could be modeled as a grid
296  with 1 row and x columns - this function calculates the position of a single
297  button in that grid, based on the layout.
298 
299  This function returns a proplist with the properties: Left, Right, Top, Bottom.
300  The proplist can be added to a GUI proplist in order to define the position
301  of said GUI element - simply merge with AddProperties(element, position);
302 
303  @par layout A proplist with prototype GUI_GridCellLayout; Alternatively,
304              you can use a the prototype GUI_BoxLayout (In that case, however,
305              the margin and alignment are shared between cells and the grid,
306              and you need two additional properties "Rows" and "Columns")
307 
308  @par row The vertical position of the element in the grid. First row is 0.
309  @par column The horizontal position of the element in the grid. First column is 0.
310  */
GuiCalculateGridElementPosition(proplist layout,int row,int column)311 global func GuiCalculateGridElementPosition(proplist layout, int row, int column)
312 {
313 	// determine internal layout
314 	var grid_layout, cell_layout;
315 
316 	if (layout["Grid"] && layout["Cell"])
317 	{
318 		grid_layout = layout["Grid"];
319 		cell_layout = layout["Cell"];
320 	}
321 	else
322 	{
323 		grid_layout = {Prototype = layout};
324 		cell_layout = {Prototype = layout};
325 		grid_layout.Rows = grid_layout.Rows;
326 		grid_layout.Columns = grid_layout.Columns;
327 	}
328 
329 	// determine position of the cell in the grid
330 	var cell_width = cell_layout.Width + cell_layout.Margin.Left + cell_layout.Margin.Right;
331 	var cell_height = cell_layout.Height + cell_layout.Margin.Top + cell_layout.Margin.Bottom;
332 
333 	var cell_pos_x = cell_layout.Margin.Left + column * cell_width;
334 	var cell_pos_y = cell_layout.Margin.Top + row * cell_height;
335 
336 	// determine position of the grid
337 	var grid_width = cell_width * (grid_layout.Columns ?? 1);
338 	var grid_height = cell_height * (grid_layout.Rows ?? 1);
339 
340 	var grid_position = GuiCalculateBoxElementPosition({Prototype = grid_layout, Width = grid_width, Height = grid_height});
341 
342 	// merge everything into one
343 	return
344 	{
345 		Left =   Format("%s%s", grid_position.Left, Call(cell_layout.Dimension, cell_pos_x)),
346 		Top =    Format("%s%s", grid_position.Top, Call(cell_layout.Dimension, cell_pos_y)),
347 		Right =  Format("%s%s", grid_position.Left, Call(cell_layout.Dimension, cell_pos_x + cell_layout.Width)),
348 		Bottom = Format("%s%s", grid_position.Top, Call(cell_layout.Dimension, cell_pos_y + cell_layout.Height))
349 	};
350 }
351