verify: implement P_Array_Append_One
[ajla.git] / stdlib / ui / widget / dialog.ajla
blob09f42478bbb70b86b72b282434049c20db53cd62
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.dialog;
21 uses ui.widget.common;
23 type dialog_state;
25 record dialog_entry [
26         const cls : widget_class;
27         init : fn(world, appstate, wid) : (world, appstate, cls.t);
28         hotkeys : treeset(char);
31 fn dialog_no_event(w : world, app : appstate, com : widget_common, wev : wevent, ids : list(wid)) : (world, appstate, widget_common, bool);
32 fn dialog_init(label : string, entries : list(dialog_entry), selected : int, process_event : fn(world, appstate, widget_common, wevent, list(wid)) : (world, appstate, widget_common, bool), layout : fn(appstate, list(wid), int, int, int, int, int) : (appstate, int, int), color_scheme : bytes, w : world, app : appstate, id : wid) : (world, appstate, dialog_state);
33 fn dialog_redraw(app : appstate, curs : curses, com : widget_common, st : dialog_state) : curses;
34 fn dialog_get_cursor(implicit app : appstate, implicit com : widget_common, implicit st : dialog_state) : (int, int);
35 fn dialog_process_event(w : world, app : appstate, com : widget_common, st : dialog_state, wev : wevent) : (world, appstate, widget_common, dialog_state);
37 const dialog_class~flat := widget_class.[
38         t : dialog_state,
39         name : "dialog",
40         is_selectable : true,
41         redraw : dialog_redraw,
42         get_cursor : dialog_get_cursor,
43         process_event : dialog_process_event,
46 implementation
48 const horizontal_outer_border := 3;
49 const horizontal_inner_border := 2;
50 const vertical_outer_border := 1;
51 const border_width := 2;
53 record dialog_state [
54         label : string;
55         label_len : int;
56         color_scheme : bytes;
57         no_controls : bool;
58         entries : list(dialog_entry);
59         ids : list(wid);
60         process_event : fn(world, appstate, widget_common, wevent, list(wid)) : (world, appstate, widget_common, bool);
61         layout : fn(appstate, list(wid), int, int, int, int, int) : (appstate, int, int);
64 fn dialog_no_event(implicit w : world, implicit app : appstate, implicit com : widget_common, wev : wevent, ids : list(wid)) : (world, appstate, widget_common, bool)
66         return false;
69 fn dialog_init(label : string, entries : list(dialog_entry), selected : int, process_event : fn(world, appstate, widget_common, wevent, list(wid)) : (world, appstate, widget_common, bool), layout : fn(appstate, list(wid), int, int, int, int, int) : (appstate, int, int), color_scheme : bytes, implicit w : world, implicit app : appstate, id : wid) : (world, appstate, dialog_state)
71         var no_controls := false;
72         if selected = -1 then [
73                 no_controls := true;
74                 selected := 0;
75         ]
76         var ids := fill(wid, wid_none, len(entries));
77         var i := selected - 1;
78         while i >= 0 do [
79                 const entry := entries[i];
80                 var sid := widget_new(id, entry.cls, entry.init, false);
81                 ids[i] := sid;
82                 i -= 1;
83         ]
84         i := len(entries) - 1;
85         while i >= selected do [
86                 const entry := entries[i];
87                 var sid := widget_new(id, entry.cls, entry.init, false);
88                 ids[i] := sid;
89                 i -= 1;
90         ]
91         label := ` ` + label + ` `;
92         var st := dialog_state.[
93                 label : label,
94                 label_len : string_length(label),
95                 color_scheme : color_scheme,
96                 no_controls : no_controls,
97                 entries : entries,
98                 ids : ids,
99                 process_event : process_event,
100                 layout : layout,
101         ];
102         return st;
105 fn dialog_redraw(implicit app : appstate, implicit curs : curses, com : widget_common, st : dialog_state) : curses
107         property_set_attrib(property_get_attrib(st.color_scheme + "dialog", #0000, #0000, #0000, #aaaa, #aaaa, #aaaa, 0, curses_invert));
108         curses_fill_rect(0, com.size_x, 0, com.size_y, ' ');
109         curses_box(horizontal_outer_border, com.size_x - horizontal_outer_border - 1, vertical_outer_border, com.size_y - vertical_outer_border - 1, border_width);
110         curses_set_pos((com.size_x - st.label_len) shr 1, vertical_outer_border);
111         property_set_attrib(property_get_attrib(st.color_scheme + "dialog-title", #aaaa, #aaaa, #aaaa, #0000, #0000, #0000, 0, 0));
112         curses_print(st.label);
113         curses_restrict_viewport(horizontal_outer_border + 1, com.size_x - horizontal_outer_border - 1, vertical_outer_border + 1, com.size_y - vertical_outer_border - 1, com.off_x, com.off_y);
114         widget_redraw_subwidgets(com);
115         curses_revert_viewport();
118 fn dialog_get_cursor(implicit app : appstate, implicit com : widget_common, implicit st : dialog_state) : (int, int)
120         if len(com.sub_widgets) > 0 then [
121                 var top := com.sub_widgets[len(com.sub_widgets) - 1];
122                 var x, y := widget_get_cursor(app, top);
123                 if x >= horizontal_outer_border + 1 - com.off_x, x < com.size_x - horizontal_outer_border - 1 - com.off_x, y >= vertical_outer_border + 1 - com.off_y, y < com.size_y - vertical_outer_border - 1 - com.off_y then
124                         return x, y;
125         ]
126         return -1, -1;
129 fn dialog_scroll(implicit app : appstate, implicit com : widget_common, implicit st : dialog_state) : (widget_common, dialog_state, bool)
131         var ret := false;
132         if not st.no_controls, len(com.sub_widgets) > 0 then [
133                 var top := widget_get_common(com.sub_widgets[len(com.sub_widgets) - 1]);
134                 if com.off_y + top.y + top.size_y > com.size_y - vertical_outer_border - 1 then [
135                         com.off_y := -(top.y + top.size_y - (com.size_y - vertical_outer_border - 1));
136                         ret := true;
137                 ]
138                 if com.off_y + top.y < vertical_outer_border + 1 then [
139                         com.off_y := -(top.y - vertical_outer_border - 1);
140                         ret := true;
141                 ]
142         ]
143         return ret;
146 fn dialog_process_event(implicit w : world, implicit app : appstate, implicit com : widget_common, implicit st : dialog_state, wev : wevent) : (world, appstate, widget_common, dialog_state)
148         if wev is resize then [
149                 var x_border := (horizontal_outer_border + 1 + horizontal_inner_border) * 2;
150                 var pref_x := (wev.resize.x - x_border) * 9 div 10;
151                 var xs, ys := st.layout(st.ids, x_border shr 1, vertical_outer_border + 1, string_length(st.label) - 2, pref_x, wev.resize.x - x_border);
152                 com.size_x := xs + x_border;
153                 com.size_y := ys + vertical_outer_border + 1;
154                 com.size_y := min(com.size_y, wev.resize.y - 1);
155                 com.x := (wev.resize.x - com.size_x) shr 1;
156                 com.y := (wev.resize.y - com.size_y) shr 1;
157                 com.off_y := 0;
158                 var sc := dialog_scroll();
159                 goto redraw;
160         ]
161         if wev is change_focus then [
162                 if widget_is_top(com.self) then [
163                         property_set("fkeys", property.l.([
164                                 property.s.(``),
165                                 property.s.(``),
166                                 property.s.(``),
167                                 property.s.(``),
168                                 property.s.(``),
169                                 property.s.(``),
170                                 property.s.(``),
171                                 property.s.(``),
172                                 property.s.(``),
173                                 property.s.(`Cancel`),
174                         ]));
175                 ]
176                 return;
177         ]
178         var processed := st.process_event(wev, st.ids);
179         if processed then
180                 goto activated;
181         if st.no_controls then [
182                 if wev is keyboard, wev.keyboard.key = key_esc or wev.keyboard.key = key_enter or wev.keyboard.key = key_f10 or wev.keyboard.key = ' ' then [
183                         widget_enqueue_event(com.self, wevent.close);
184                         return;
185                 ]
186                 if wev is mouse, (wev.mouse.buttons and not wev.mouse.prev_buttons) <> 0 then [
187                         widget_enqueue_event(com.self, wevent.close);
188                         return;
189                 ]
190         ]
191         var forwarding := widget_should_forward(wev);
192         widget_forward(wev);
193         if not forwarding, wev is keyboard then [
194                 for i in list_iterator_reverse(st.entries) do [
195                         if not is_uninitialized_record(st.entries[i].hotkeys), treeset_test(st.entries[i].hotkeys, char_upcase(wev.keyboard.key)) then [
196                                 var id := st.ids[i];
197                                 widget_enqueue_event(id, wevent.keyboard.(event_keyboard.[
198                                         key : key_enter,
199                                         flags : 0,
200                                         rep : 1,
201                                 ]));
202                                 widget_activate(id);
203                                 goto activated;
204                         ]
205                 ]
206         ]
207 activated:
208         var sc := dialog_scroll();
209         if sc then
210                 goto redraw;
211         return;
213 redraw:
214         widget_enqueue_event(com.self, wevent.redraw.(event_redraw.[
215                 x1 : 0,
216                 x2 : com.size_x,
217                 y1 : 0,
218                 y2 : com.size_y,
219         ]));