2 * Copyright (C) 2024 Mikulas Patocka
4 * This file is part of Ajla.
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
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.
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/>.
19 private unit ui.widget.menu;
21 uses ui.widget.common;
28 click : fn(world, appstate, wid) : (world, appstate);
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.[
43 get_cursor : menu_get_cursor,
44 get_pivot : menu_get_pivot,
45 process_event : menu_process_event,
52 entries : list(menu_entry);
54 hotkey_index : list(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 .. ];
69 if hotkeys[j] = key then
71 hotkeys[i] := char_upcase(key);
72 hotkey_index[i] := idx;
77 while is_uninitialized_record(entries[selected].click) do
80 color_scheme : color_scheme,
83 hotkey_index : hotkey_index,
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);
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);
103 property_set_attrib(prop_selected);
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);
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 .. ]);
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);
134 curses_revert_viewport();
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
165 var i := st.selected;
168 if i < 0 or i >= len(st.entries) then
171 if not is_uninitialized_record(st.entries[i].click) then [
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 [
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);
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;
208 com.size_x := size_x;
209 com.size_y := size_y;
210 menu_fixup_scrolled();
213 if wev is change_focus then [
214 if widget_is_top(com.self) then [
215 property_set("fkeys", property.l.([
225 property.s.(`Cancel`),
230 if wev is keyboard then [
231 if wev.keyboard.key = key_up then [
235 if wev.keyboard.key = key_down then [
239 if wev.keyboard.key = key_home then [
240 menu_up_down(-len(st.entries));
243 if wev.keyboard.key = key_end then [
244 menu_up_down(len(st.entries));
247 if wev.keyboard.key = key_page_up then [
248 menu_up_down(-(com.size_y - 2));
251 if wev.keyboard.key = key_page_down then [
252 menu_up_down(com.size_y - 2);
255 if wev.keyboard.key = key_enter or wev.keyboard.key = ' ' then [
257 if len(st.entries) <> 0 then [
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.[
273 wev : wevent.keyboard.(event_keyboard.[
279 widget_enqueue_events(events);
282 if wev.keyboard.key = key_left then
284 if wev.keyboard.key = key_right then
287 if wev.keyboard.key = key_esc or wev.keyboard.key = key_f10 then [
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);
293 widget_enqueue_event(com.self, wevent.close);
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 [
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 [
312 widget_enqueue_event(com.parent, wev);
313 widget_enqueue_event(com.self, wevent.close);
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;
324 mmid := widget_get_underlying(mmid);
325 if not wid_is_valid(mmid) then
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
337 if wev.mouse.buttons = 0, wev.mouse.prev_buttons <> 0 then [
346 widget_enqueue_event(com.self, wevent.redraw.(event_redraw.[