Ajla 0.1.0
[ajla.git] / newlib / ui / widget / menu.ajla
blob24c1e2a374324219162611b85cdce512efdb51e8
1 {*
2  * Copyright (C) 2024 Mikulas Patocka
3  *
4  * This file is part of Ajla.
5  *
6  * Ajla is free software: you can redistribute it and/or modify it under the
7  * terms of the GNU General Public License as published by the Free Software
8  * Foundation, either version 3 of the License, or (at your option) any later
9  * version.
10  *
11  * Ajla is distributed in the hope that it will be useful, but WITHOUT ANY
12  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13  * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along with
16  * Ajla. If not, see <https://www.gnu.org/licenses/>.
17  *}
19 private unit ui.widget.menu;
21 uses ui.widget.common;
23 type menu_state;
25 record menu_entry [
26         label : string;
27         label_right : string;
28         click : fn(world, appstate, wid) : (world, appstate);
29         close_onclick : bool;
32 fn menu_init(color_scheme : bytes, entries : list(menu_entry), w : world, app : appstate, id : wid) : (world, appstate, menu_state);
33 fn menu_redraw(app : appstate, curs : curses, com : widget_common, st : menu_state) : curses;
34 fn menu_get_cursor(app : appstate, com : widget_common, st : menu_state) : (int, int);
35 fn menu_get_pivot(app : appstate, com : widget_common, st : menu_state) : (int, int);
36 fn menu_process_event(w : world, app : appstate, com : widget_common, st : menu_state, wev : wevent) : (world, appstate, widget_common, menu_state);
38 const menu_class~flat := widget_class.[
39         t : menu_state,
40         name : "menu",
41         is_selectable : true,
42         redraw : menu_redraw,
43         get_cursor : menu_get_cursor,
44         get_pivot : menu_get_pivot,
45         process_event : menu_process_event,
48 implementation
50 record menu_state [
51         color_scheme : bytes;
52         entries : list(menu_entry);
53         hotkeys : list(char);
54         hotkey_index : list(int);
55         selected : int;
56         scrolled : int;
59 fn menu_init(color_scheme : bytes, entries : list(menu_entry), implicit w : world, implicit app : appstate, id : wid) : (world, appstate, menu_state)
61         var hotkeys := fill(char, 0, len(entries));
62         var hotkey_index := fill(-1, len(entries));
63         for i := 0 to len(entries) do [
64                 var idx := list_search(entries[i].label, '~');
65                 if idx >= 0, idx < len(entries[i].label) - 1 then [
66                         var key := entries[i].label[idx + 1];
67                         entries[i].label := entries[i].label[ .. idx] + entries[i].label[idx + 1 .. ];
68                         for j := 0 to i do
69                                 if hotkeys[j] = key then
70                                         goto skip_hotkey;
71                         hotkeys[i] := char_upcase(key);
72                         hotkey_index[i] := idx;
73 skip_hotkey:
74                 ]
75         ]
76         var selected := 0;
77         while is_uninitialized_record(entries[selected].click) do
78                 selected += 1;
79         return menu_state.[
80                 color_scheme : color_scheme,
81                 entries : entries,
82                 hotkeys : hotkeys,
83                 hotkey_index : hotkey_index,
84                 selected : selected,
85                 scrolled : 0,
86         ];
89 fn menu_redraw(implicit app : appstate, implicit curs : curses, com : widget_common, st : menu_state) : curses
91         var prop_frame := property_get_attrib(st.color_scheme + "menu-frame", #0000, #0000, #0000, #aaaa, #aaaa, #aaaa, 0, curses_invert);
92         var prop_text := property_get_attrib(st.color_scheme + "menu-text", #0000, #0000, #0000, #aaaa, #aaaa, #aaaa, 0, curses_invert);
93         var prop_text_hotkey := property_get_attrib(st.color_scheme + "menu-text-hotkey", #aaaa, #aaaa, #aaaa, #0000, #0000, #0000, 0, 0);
94         var prop_selected := property_get_attrib(st.color_scheme + "menu-text-selected", #aaaa, #aaaa, #aaaa, #0000, #0000, #0000, 0, 0);
95         var prop_selected_hotkey := property_get_attrib(st.color_scheme + "menu-text-selected-hotkey", #aaaa, #aaaa, #aaaa, #0000, #0000, #0000, 0, 0);
96         property_set_attrib(prop_frame);
97         curses_box(0, com.size_x - 1, 0, com.size_y - 1, 1);
98         var ypos := 1;
99         for i := st.scrolled to st.scrolled + com.size_y - 2 do [
100                 if i <> st.selected then [
101                         property_set_attrib(prop_text);
102                 ] else [
103                         property_set_attrib(prop_selected);
104                 ]
105                 curses_fill_rect(1, com.size_x - 1, ypos, ypos + 1, ' ');
106                 if i < len(st.entries) then [
107                         var label := st.entries[i].label;
108                         if len(label) = 0 then [
109                                 property_set_attrib(prop_frame);
110                                 curses_hline(1, com.size_x - 1, ypos, 1);
111                                 curses_frame(0, ypos, #0111);
112                                 curses_frame(com.size_x - 1, ypos, #1101);
113                                 goto cont;
114                         ]
115                         var label_right := st.entries[i].label_right;
116                         var label_right_len := string_length(label_right);
117                         curses_restrict_viewport(2, com.size_x - 2, ypos, ypos + 1, 0, 0);
118                         curses_set_pos(2, ypos);
119                         if st.hotkey_index[i] >= 0 then [
120                                 curses_print(label[ .. st.hotkey_index[i]]);
121                                 property_set_attrib(select(i = st.selected, prop_text_hotkey, prop_selected_hotkey));
122                                 curses_print([ label[st.hotkey_index[i]] ]);
123                                 property_set_attrib(select(i = st.selected, prop_text, prop_selected));
124                                 curses_print(label[st.hotkey_index[i] + 1 .. ]);
125                         ] else [
126                                 curses_print(label);
127                         ]
128                         var pos_x, pos_y := curses_get_pos();
129                         var pos_right := com.size_x - 2 - label_right_len;
130                         if pos_x + 2 <= pos_right then [
131                                 curses_set_pos(pos_right, ypos);
132                                 curses_print(label_right);
133                         ]
134                         curses_revert_viewport();
135                 ]
136 cont:
137                 ypos += 1;
138         ]
141 fn menu_get_cursor(app : appstate, com : widget_common, st : menu_state) : (int, int)
143         return 1, 1 + st.selected - st.scrolled;
146 fn menu_get_pivot(app : appstate, com : widget_common, st : menu_state) : (int, int)
148         return com.size_x, 1 + st.selected - st.scrolled;
151 fn menu_fixup_scrolled(implicit com : widget_common, implicit st : menu_state) : menu_state
153         if st.selected > st.scrolled + com.size_y - 3 then
154                 st.scrolled := st.selected - (com.size_y - 3);
155         if st.selected < st.scrolled then
156                 st.scrolled := st.selected;
157         if st.scrolled + com.size_y - 2 > len(st.entries) then
158                 st.scrolled := len(st.entries) - (com.size_y - 2);
161 fn menu_up_down(implicit com : widget_common, implicit st : menu_state, num : int) : menu_state
163         var dir := sgn(num);
164         num := abs(num);
165         var i := st.selected;
166         while true do [
167                 i += dir;
168                 if i < 0 or i >= len(st.entries) then
169                         break;
170                 num -= 1;
171                 if not is_uninitialized_record(st.entries[i].click) then [
172                         st.selected := i;
173                         if num <= 0 then
174                                 break;
175                 ]
176         ]
177         menu_fixup_scrolled();
180 fn menu_click(implicit w : world, implicit app : appstate, com : widget_common, st : menu_state) : (world, appstate)
182         st.entries[st.selected].click(com.self);
183         if st.entries[st.selected].close_onclick then
184                 widget_destroy_onclick(com.self);
187 fn menu_process_event(implicit w : world, implicit app : appstate, implicit com : widget_common, implicit st : menu_state, wev : wevent) : (world, appstate, widget_common, menu_state)
189         if wev is resize then [
190                 var size_x := 0;
191                 for i := 0 to len(st.entries) do [
192                         var s := string_length(st.entries[i].label);
193                         if st.entries[i].label_right <> `` then
194                                 s += 2 + string_length(st.entries[i].label_right);
195                         size_x := max(size_x, s);
196                 ]
197                 size_x += 4;
198                 var size_y := 2 + len(st.entries);
199                 size_x := min(size_x, wev.resize.x);
200                 size_y := min(size_y, wev.resize.y);
201                 var pivot_x, pivot_y := widget_get_pivot(com.self);
202                 if pivot_x + size_x > wev.resize.x then
203                         pivot_x := wev.resize.x - size_x;
204                 if pivot_y + size_y > wev.resize.y then
205                         pivot_y := wev.resize.y - size_y;
206                 com.x := pivot_x;
207                 com.y := pivot_y;
208                 com.size_x := size_x;
209                 com.size_y := size_y;
210                 menu_fixup_scrolled();
211                 goto redraw;
212         ]
213         if wev is change_focus then [
214                 if widget_is_top(com.self) then [
215                         property_set("fkeys", property.l.([
216                                 property.s.(``),
217                                 property.s.(``),
218                                 property.s.(``),
219                                 property.s.(``),
220                                 property.s.(``),
221                                 property.s.(``),
222                                 property.s.(``),
223                                 property.s.(``),
224                                 property.s.(``),
225                                 property.s.(`Cancel`),
226                         ]));
227                 ]
228                 return;
229         ]
230         if wev is keyboard then [
231                 if wev.keyboard.key = key_up then [
232                         menu_up_down(-1);
233                         goto redraw;
234                 ]
235                 if wev.keyboard.key = key_down then [
236                         menu_up_down(1);
237                         goto redraw;
238                 ]
239                 if wev.keyboard.key = key_home then [
240                         menu_up_down(-len(st.entries));
241                         goto redraw;
242                 ]
243                 if wev.keyboard.key = key_end then [
244                         menu_up_down(len(st.entries));
245                         goto redraw;
246                 ]
247                 if wev.keyboard.key = key_page_up then [
248                         menu_up_down(-(com.size_y - 2));
249                         goto redraw;
250                 ]
251                 if wev.keyboard.key = key_page_down then [
252                         menu_up_down(com.size_y - 2);
253                         goto redraw;
254                 ]
255                 if wev.keyboard.key = key_enter or wev.keyboard.key = ' ' then [
256 do_click:
257                         if len(st.entries) <> 0 then [
258                                 menu_click();
259                                 return;
260                         ]
261                 ]
262                 if wev.keyboard.key = key_left or wev.keyboard.key = key_right then [
263                         var u := widget_get_underlying(com.self);
264                         if wid_is_valid(u), widget_get_class(u) = "mainmenu" then [
265                                 var events := [ wid_wevent.[
266                                         id : com.self,
267                                         wev : wevent.close,
268                                 ], wid_wevent.[
269                                         id : u,
270                                         wev : wev,
271                                 ], wid_wevent.[
272                                         id : u,
273                                         wev : wevent.keyboard.(event_keyboard.[
274                                                 key : key_enter,
275                                                 flags : 0,
276                                                 rep : 1,
277                                         ]),
278                                 ] ];
279                                 widget_enqueue_events(events);
280                                 return;
281                         ]
282                         if wev.keyboard.key = key_left then
283                                 goto do_esc;
284                         if wev.keyboard.key = key_right then
285                                 goto do_click;
286                 ]
287                 if wev.keyboard.key = key_esc or wev.keyboard.key = key_f10 then [
288 do_esc:
289                         var mmid := widget_get_underlying(com.self);
290                         if wid_is_valid(mmid), widget_get_class(mmid) = "mainmenu" then [
291                                 widget_enqueue_event(mmid, wevent.close);
292                         ]
293                         widget_enqueue_event(com.self, wevent.close);
294                         return;
295                 ]
296                 if wev.keyboard.flags = 0 then [
297                         for i := 0 to len(st.hotkeys) do [
298                                 if st.hotkeys[i] = char_upcase(wev.keyboard.key) then [
299                                         st.selected := i;
300                                         menu_click();
301                                         goto redraw;
302                                 ]
303                         ]
304                 ]
305                 return;
306         ]
307         if wev is mouse, wev.mouse.buttons <> 0 or wev.mouse.prev_buttons <> 0 then [
308                 var mx, my := widget_relative_mouse_coords(com.self, wev.mouse);
309                 if mx < 0 or mx >= com.size_x or my < 0 or my >= com.size_y then [
310                         if (wev.mouse.buttons and not wev.mouse.prev_buttons) <> 0 then [
311 forward_to_parent:
312                                 widget_enqueue_event(com.parent, wev);
313                                 widget_enqueue_event(com.self, wevent.close);
314                                 return;
315                         ]
316                         if wev.mouse.buttons <> 0 then [
317                                 var mmid := com.self;
318                                 while widget_get_class(mmid) = "menu" or widget_get_class(mmid) = "mainmenu" do [
319                                         var mainmenu := widget_get_common(mmid);
320                                         var mmx, mmy := widget_relative_mouse_coords(mainmenu.self, wev.mouse);
321                                         if mmx >= 0, mmx < mainmenu.size_x, mmy >= 0, mmy < mainmenu.size_y then [
322                                                 goto forward_to_parent;
323                                         ]
324                                         mmid := widget_get_underlying(mmid);
325                                         if not wid_is_valid(mmid) then
326                                                 break;
327                                 ]
328                         ]
329                         return;
330                 ]
331                 if mx >= 1, mx < com.size_x - 1, my >= 1, my < com.size_y - 1 then [
332                         var sel := my - 1 + st.scrolled;
333                         if sel >= 0, sel < len(st.entries) then [
334                                 if is_uninitialized_record(st.entries[sel].click) then
335                                         return;
336                                 st.selected := sel;
337                                 if wev.mouse.buttons = 0, wev.mouse.prev_buttons <> 0 then [
338                                         menu_click();
339                                 ]
340                                 goto redraw;
341                         ]
342                 ]
343                 return;
344         ]
345 redraw:
346         widget_enqueue_event(com.self, wevent.redraw.(event_redraw.[
347                 x1 : 0,
348                 x2 : com.size_x,
349                 y1 : 0,
350                 y2 : com.size_y,
351         ]));