source: mainline/uspace/lib/ui/src/menu.c@ 1af103e

ticket/834-toolchain-update topic/msim-upgrade topic/simplify-dev-export
Last change on this file since 1af103e was 46bd63c9, checked in by Jiri Svoboda <jiri@…>, 2 years ago

Split drop-down menu into two classes: drop-down and menu

Naming is clearly the hardest problem in computer science.

  • Property mode set to 100644
File size: 14.0 KB
RevLine 
[214aefb]1/*
[5d380b6]2 * Copyright (c) 2023 Jiri Svoboda
[214aefb]3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * - Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 * - Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * - The name of the author may not be used to endorse or promote products
15 * derived from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29/** @addtogroup libui
30 * @{
31 */
32/**
33 * @file Menu
34 */
35
36#include <adt/list.h>
37#include <errno.h>
38#include <gfx/color.h>
39#include <gfx/context.h>
40#include <gfx/render.h>
41#include <gfx/text.h>
42#include <io/pos_event.h>
43#include <stdlib.h>
44#include <str.h>
[96c6a00]45#include <uchar.h>
[46bd63c9]46#include <ui/ui.h>
[96c6a00]47#include <ui/accel.h>
[214aefb]48#include <ui/control.h>
49#include <ui/paint.h>
[3c8c580]50#include <ui/popup.h>
[214aefb]51#include <ui/menu.h>
52#include <ui/menuentry.h>
53#include <ui/resource.h>
[c68c18b9]54#include <ui/window.h>
[214aefb]55#include "../private/menu.h"
56#include "../private/resource.h"
57
58enum {
59 menu_frame_w = 4,
60 menu_frame_h = 4,
61 menu_frame_w_text = 2,
[ff6e91b]62 menu_frame_h_text = 1,
63 menu_frame_h_margin_text = 1
[214aefb]64};
65
[d7f82635]66static void ui_menu_popup_close(ui_popup_t *, void *);
[59768c7]67static void ui_menu_popup_kbd(ui_popup_t *, void *, kbd_event_t *);
[3c8c580]68static void ui_menu_popup_pos(ui_popup_t *, void *, pos_event_t *);
[5de852c]69static void ui_menu_key_press_unmod(ui_menu_t *, kbd_event_t *);
[3c8c580]70
71static ui_popup_cb_t ui_menu_popup_cb = {
[d7f82635]72 .close = ui_menu_popup_close,
[59768c7]73 .kbd = ui_menu_popup_kbd,
[3c8c580]74 .pos = ui_menu_popup_pos
75};
76
[214aefb]77/** Create new menu.
78 *
[46bd63c9]79 * @param parent Parent window
[214aefb]80 * @param mbar Menu bar
81 * @param rmenu Place to store pointer to new menu
82 * @return EOK on success, ENOMEM if out of memory
83 */
[46bd63c9]84errno_t ui_menu_create(ui_window_t *parent, ui_menu_t **rmenu)
[214aefb]85{
86 ui_menu_t *menu;
87
88 menu = calloc(1, sizeof(ui_menu_t));
89 if (menu == NULL)
90 return ENOMEM;
91
[46bd63c9]92 menu->parent = parent;
[214aefb]93 list_initialize(&menu->entries);
94
95 *rmenu = menu;
96 return EOK;
97}
98
99/** Destroy menu.
100 *
101 * @param menu Menu or @c NULL
102 */
103void ui_menu_destroy(ui_menu_t *menu)
104{
105 ui_menu_entry_t *mentry;
106
107 if (menu == NULL)
108 return;
109
110 /* Destroy entries */
111 mentry = ui_menu_entry_first(menu);
112 while (mentry != NULL) {
113 ui_menu_entry_destroy(mentry);
114 mentry = ui_menu_entry_first(menu);
115 }
116
117 free(menu->caption);
118 free(menu);
119}
120
[46bd63c9]121/** Set menu callbacks.
[214aefb]122 *
123 * @param menu Menu
[46bd63c9]124 * @param cb Callbacks
125 * @param arg Callback argument
[214aefb]126 */
[46bd63c9]127void ui_menu_set_cb(ui_menu_t *menu, ui_menu_cb_t *cb, void *arg)
[214aefb]128{
[46bd63c9]129 menu->cb = cb;
130 menu->arg = arg;
[214aefb]131}
132
133/** Get menu geometry.
134 *
135 * @param menu Menu
136 * @param spos Starting position
137 * @param geom Structure to fill in with computed geometry
138 */
139void ui_menu_get_geom(ui_menu_t *menu, gfx_coord2_t *spos,
140 ui_menu_geom_t *geom)
141{
142 gfx_coord2_t edim;
143 gfx_coord_t frame_w;
144 gfx_coord_t frame_h;
[46bd63c9]145 ui_resource_t *res;
[214aefb]146
[46bd63c9]147 res = ui_window_get_res(menu->parent);
[c68c18b9]148
149 if (res->textmode) {
[214aefb]150 frame_w = menu_frame_w_text;
151 frame_h = menu_frame_h_text;
152 } else {
153 frame_w = menu_frame_w;
154 frame_h = menu_frame_h;
155 }
156
[b8b64a8]157 edim.x = ui_menu_entry_calc_width(menu, menu->max_caption_w,
158 menu->max_shortcut_w);
[f251883]159 edim.y = menu->total_h;
[214aefb]160
161 geom->outer_rect.p0 = *spos;
162 geom->outer_rect.p1.x = spos->x + edim.x + 2 * frame_w;
163 geom->outer_rect.p1.y = spos->y + edim.y + 2 * frame_h;
164
165 geom->entries_rect.p0.x = spos->x + frame_w;
166 geom->entries_rect.p0.y = spos->y + frame_h;
167 geom->entries_rect.p1.x = geom->entries_rect.p0.x + edim.x;
168 geom->entries_rect.p1.y = geom->entries_rect.p0.x + edim.y;
169}
170
[3c8c580]171/** Get UI resource from menu.
172 *
173 * @param menu Menu
174 * @return UI resource
175 */
176ui_resource_t *ui_menu_get_res(ui_menu_t *menu)
177{
178 return ui_popup_get_res(menu->popup);
179}
180
181/** Open menu.
182 *
183 * @param menu Menu
184 * @param prect Parent rectangle around which the menu should be placed
[5d380b6]185 * @param idev_id Input device associated with the menu's seat
[3c8c580]186 */
[5d380b6]187errno_t ui_menu_open(ui_menu_t *menu, gfx_rect_t *prect, sysarg_t idev_id)
[3c8c580]188{
189 ui_popup_t *popup = NULL;
190 ui_popup_params_t params;
191 ui_menu_geom_t geom;
192 gfx_coord2_t mpos;
193 errno_t rc;
194
[59768c7]195 /* Select first entry */
196 menu->selected = ui_menu_entry_first(menu);
197
[3c8c580]198 /* Determine menu dimensions */
199
200 mpos.x = 0;
201 mpos.y = 0;
202 ui_menu_get_geom(menu, &mpos, &geom);
203
204 ui_popup_params_init(&params);
205 params.rect = geom.outer_rect;
[c9927c66]206 params.place = *prect;
[5d380b6]207 params.idev_id = idev_id;
[3c8c580]208
[46bd63c9]209 rc = ui_popup_create(ui_window_get_ui(menu->parent), menu->parent,
210 &params, &popup);
[3c8c580]211 if (rc != EOK)
212 return rc;
213
214 menu->popup = popup;
215 ui_popup_set_cb(popup, &ui_menu_popup_cb, menu);
216
217 return ui_menu_paint(menu, &mpos);
218}
219
220/** Close menu.
221 *
222 * @param menu Menu
223 */
224void ui_menu_close(ui_menu_t *menu)
225{
226 ui_popup_destroy(menu->popup);
227 menu->popup = NULL;
228}
229
[59768c7]230/** Determine if menu is open.
231 *
232 * @param menu Menu
233 * @return @c true iff menu is open
234 */
235bool ui_menu_is_open(ui_menu_t *menu)
236{
237 return menu->popup != NULL;
238}
239
[214aefb]240/** Paint menu.
241 *
242 * @param menu Menu
243 * @param spos Starting position (top-left corner)
244 * @return EOK on success or an error code
245 */
[ff6e91b]246errno_t ui_menu_paint_bg_gfx(ui_menu_t *menu, gfx_coord2_t *spos)
[214aefb]247{
248 ui_resource_t *res;
249 ui_menu_geom_t geom;
250 gfx_rect_t bg_rect;
251 errno_t rc;
252
[3c8c580]253 res = ui_menu_get_res(menu);
[214aefb]254 ui_menu_get_geom(menu, spos, &geom);
255
256 /* Paint menu frame */
257
258 rc = gfx_set_color(res->gc, res->wnd_face_color);
259 if (rc != EOK)
260 goto error;
261
262 rc = ui_paint_outset_frame(res, &geom.outer_rect, &bg_rect);
263 if (rc != EOK)
264 goto error;
265
266 /* Paint menu background */
267
268 rc = gfx_set_color(res->gc, res->wnd_face_color);
269 if (rc != EOK)
270 goto error;
271
272 rc = gfx_fill_rect(res->gc, &bg_rect);
273 if (rc != EOK)
274 goto error;
275
[ff6e91b]276 return EOK;
277error:
278 return rc;
279}
280
281/** Paint menu.
282 *
283 * @param menu Menu
284 * @param spos Starting position (top-left corner)
285 * @return EOK on success or an error code
286 */
287errno_t ui_menu_paint_bg_text(ui_menu_t *menu, gfx_coord2_t *spos)
288{
289 ui_resource_t *res;
290 ui_menu_geom_t geom;
291 gfx_rect_t rect;
292 errno_t rc;
293
294 res = ui_menu_get_res(menu);
295 ui_menu_get_geom(menu, spos, &geom);
296
297 /* Paint menu background */
298
299 rc = gfx_set_color(res->gc, res->wnd_face_color);
300 if (rc != EOK)
301 goto error;
302
303 rc = gfx_fill_rect(res->gc, &geom.outer_rect);
304 if (rc != EOK)
305 goto error;
306
307 /* Paint menu box */
308
309 rect = geom.outer_rect;
310 rect.p0.x += menu_frame_h_margin_text;
311 rect.p1.x -= menu_frame_h_margin_text;
312
313 rc = ui_paint_text_box(res, &rect, ui_box_single, res->wnd_face_color);
314 if (rc != EOK)
315 goto error;
316
317 return EOK;
318error:
319 return rc;
320}
321
322/** Paint menu.
323 *
324 * @param menu Menu
325 * @param spos Starting position (top-left corner)
326 * @return EOK on success or an error code
327 */
328errno_t ui_menu_paint(ui_menu_t *menu, gfx_coord2_t *spos)
329{
330 ui_resource_t *res;
331 gfx_coord2_t pos;
332 ui_menu_entry_t *mentry;
333 ui_menu_geom_t geom;
334 errno_t rc;
335
336 res = ui_menu_get_res(menu);
337 ui_menu_get_geom(menu, spos, &geom);
338
339 /* Paint menu frame and background */
340 if (res->textmode)
341 rc = ui_menu_paint_bg_text(menu, spos);
342 else
343 rc = ui_menu_paint_bg_gfx(menu, spos);
344 if (rc != EOK)
345 goto error;
346
[214aefb]347 /* Paint entries */
348 pos = geom.entries_rect.p0;
349
350 mentry = ui_menu_entry_first(menu);
351 while (mentry != NULL) {
352 rc = ui_menu_entry_paint(mentry, &pos);
353 if (rc != EOK)
354 goto error;
355
356 pos.y += ui_menu_entry_height(mentry);
357 mentry = ui_menu_entry_next(mentry);
358 }
359
360 rc = gfx_update(res->gc);
361 if (rc != EOK)
362 goto error;
363
364 return EOK;
365error:
366 return rc;
367}
368
[f536a16]369/** Handle position event in menu.
[214aefb]370 *
371 * @param menu Menu
372 * @param spos Starting position (top-left corner)
[0262f16c]373 * @param event Position event
374 * @return ui_claimed iff the event was claimed
[214aefb]375 */
[0262f16c]376ui_evclaim_t ui_menu_pos_event(ui_menu_t *menu, gfx_coord2_t *spos,
377 pos_event_t *event)
[214aefb]378{
379 ui_menu_geom_t geom;
380 ui_menu_entry_t *mentry;
381 gfx_coord2_t pos;
[0262f16c]382 gfx_coord2_t epos;
383 ui_evclaim_t claimed;
[214aefb]384
385 ui_menu_get_geom(menu, spos, &geom);
[0262f16c]386 epos.x = event->hpos;
387 epos.y = event->vpos;
[214aefb]388
389 pos = geom.entries_rect.p0;
390
391 mentry = ui_menu_entry_first(menu);
392 while (mentry != NULL) {
[0262f16c]393 claimed = ui_menu_entry_pos_event(mentry, &pos, event);
394 if (claimed == ui_claimed)
395 return ui_claimed;
[214aefb]396
397 pos.y += ui_menu_entry_height(mentry);
398 mentry = ui_menu_entry_next(mentry);
399 }
[0262f16c]400
[f536a16]401 /* Event inside menu? */
402 if (gfx_pix_inside_rect(&epos, &geom.outer_rect)) {
403 /* Claim event */
[0262f16c]404 return ui_claimed;
[f536a16]405 } else {
406 /* Press outside menu - close it */
[d7f82635]407 if (event->type == POS_PRESS)
[46bd63c9]408 ui_menu_close_req(menu);
[f536a16]409 }
[0262f16c]410
411 return ui_unclaimed;
[214aefb]412}
413
[5de852c]414/** Handle keyboard event in menu.
[d7f82635]415 *
[5de852c]416 * @param menu Menu
417 * @param event Keyboard event
418 * @return ui_claimed iff the event was claimed
[d7f82635]419 */
[5de852c]420ui_evclaim_t ui_menu_kbd_event(ui_menu_t *menu, kbd_event_t *event)
[d7f82635]421{
[5de852c]422 if (event->type == KEY_PRESS && (event->mods &
423 (KM_CTRL | KM_ALT | KM_SHIFT)) == 0) {
424 ui_menu_key_press_unmod(menu, event);
425 }
[d7f82635]426
[b3b48f4]427 if (event->type == KEY_PRESS && (event->mods & KM_ALT) != 0 &&
428 (event->mods & (KM_CTRL | KM_SHIFT)) == 0 && event->c != '\0')
[46bd63c9]429 ui_menu_press_accel(menu, event->c, event->kbd_id);
[b3b48f4]430
[5de852c]431 return ui_claimed;
[59768c7]432}
433
434/** Move one entry up.
435 *
436 * Non-selectable entries are skipped. If we are already at the top,
437 * we wrap around.
438 *
439 * @param menu Menu
440 */
441void ui_menu_up(ui_menu_t *menu)
442{
443 gfx_coord2_t mpos;
444 ui_menu_entry_t *nentry;
445
446 if (menu->selected == NULL)
447 return;
448
449 nentry = ui_menu_entry_prev(menu->selected);
450 if (nentry == NULL)
451 nentry = ui_menu_entry_last(menu);
452
453 /* Need to find a selectable entry */
454 while (!ui_menu_entry_selectable(nentry)) {
455 nentry = ui_menu_entry_prev(nentry);
456 if (nentry == NULL)
457 nentry = ui_menu_entry_last(menu);
458
459 /* Went completely around and found nothing? */
460 if (nentry == menu->selected)
461 return;
462 }
463
464 menu->selected = nentry;
465
466 mpos.x = 0;
467 mpos.y = 0;
468 (void) ui_menu_paint(menu, &mpos);
469}
470
471/** Move one entry down.
472 *
473 * Non-selectable entries are skipped. If we are already at the bottom,
474 * we wrap around.
475 *
476 * @param menu Menu
477 */
478void ui_menu_down(ui_menu_t *menu)
479{
480 gfx_coord2_t mpos;
481 ui_menu_entry_t *nentry;
482
483 if (menu->selected == NULL)
484 return;
485
486 nentry = ui_menu_entry_next(menu->selected);
487 if (nentry == NULL)
488 nentry = ui_menu_entry_first(menu);
489
490 /* Need to find a selectable entry */
491 while (!ui_menu_entry_selectable(nentry)) {
492 nentry = ui_menu_entry_next(nentry);
493 if (nentry == NULL)
494 nentry = ui_menu_entry_first(menu);
495
496 /* Went completely around and found nothing? */
497 if (nentry == menu->selected)
498 return;
499 }
500
501 menu->selected = nentry;
502
503 mpos.x = 0;
504 mpos.y = 0;
505 (void) ui_menu_paint(menu, &mpos);
506}
507
508/** Handle key press without modifiers in menu popup window.
509 *
510 * @param menu Menu
511 * @param event Keyboard event
512 */
513static void ui_menu_key_press_unmod(ui_menu_t *menu, kbd_event_t *event)
514{
[c88d7f99]515 ui_menu_entry_t *mentry;
516 char32_t c;
517
[59768c7]518 switch (event->key) {
519 case KC_ESCAPE:
[46bd63c9]520 ui_menu_close_req(menu);
[59768c7]521 break;
522 case KC_LEFT:
[46bd63c9]523 ui_menu_left(menu, event->kbd_id);
[59768c7]524 break;
525 case KC_RIGHT:
[46bd63c9]526 ui_menu_right(menu, event->kbd_id);
[59768c7]527 break;
528 case KC_UP:
529 ui_menu_up(menu);
530 break;
531 case KC_DOWN:
532 ui_menu_down(menu);
533 break;
534 case KC_ENTER:
535 if (menu->selected != NULL)
536 ui_menu_entry_activate(menu->selected);
537 break;
538 default:
[c88d7f99]539 if (event->c != '\0') {
540 mentry = ui_menu_entry_first(menu);
541 while (mentry != NULL) {
542 c = ui_menu_entry_get_accel(mentry);
543 if (c == event->c && menu->selected != NULL) {
544 ui_menu_entry_activate(mentry);
545 break;
546 }
547 mentry = ui_menu_entry_next(mentry);
548 }
549 }
[59768c7]550 break;
551 }
552}
553
[5de852c]554/** Handle close event in menu popup window.
555 *
556 * @param popup Menu popup window
557 * @param arg Argument (ui_menu_t *)
558 */
559static void ui_menu_popup_close(ui_popup_t *popup, void *arg)
560{
561 ui_menu_t *menu = (ui_menu_t *)arg;
562
[46bd63c9]563 /* Forward close request to caller */
564 ui_menu_close_req(menu);
[5de852c]565}
566
[59768c7]567/** Handle keyboard event in menu popup window.
568 *
569 * @param popup Menu popup window
570 * @param arg Argument (ui_menu_t *)
571 * @param event Keyboard event
572 */
573static void ui_menu_popup_kbd(ui_popup_t *popup, void *arg, kbd_event_t *event)
574{
575 ui_menu_t *menu = (ui_menu_t *)arg;
576
[5de852c]577 ui_menu_kbd_event(menu, event);
[d7f82635]578}
579
[3c8c580]580/** Handle position event in menu popup window.
581 *
582 * @param popup Menu popup window
583 * @param arg Argument (ui_menu_t *)
584 * @param event Position event
585 */
586static void ui_menu_popup_pos(ui_popup_t *popup, void *arg, pos_event_t *event)
587{
588 ui_menu_t *menu = (ui_menu_t *)arg;
589 gfx_coord2_t spos;
590
591 spos.x = 0;
592 spos.y = 0;
593 ui_menu_pos_event(menu, &spos, event);
594}
595
[46bd63c9]596/** Send menu left event.
597 *
598 * @param menu Menu
599 * @param idev_id Input device ID
600 */
601void ui_menu_left(ui_menu_t *menu, sysarg_t idev_id)
602{
603 if (menu->cb != NULL && menu->cb->left != NULL)
604 menu->cb->left(menu, menu->arg, idev_id);
605}
606
607/** Send menu right event.
608 *
609 * @param menu Menu
610 * @param idev_id Input device ID
611 */
612void ui_menu_right(ui_menu_t *menu, sysarg_t idev_id)
613{
614 if (menu->cb != NULL && menu->cb->right != NULL)
615 menu->cb->right(menu, menu->arg, idev_id);
616}
617
618/** Send menu close request event.
619 *
620 * @param menu Menu
621 */
622void ui_menu_close_req(ui_menu_t *menu)
623{
624 if (menu->cb != NULL && menu->cb->close_req != NULL)
625 menu->cb->close_req(menu, menu->arg);
626}
627
628/** Send menu accelerator key press event.
629 *
630 * @param menu Menu
631 * @param c Character
632 * @param kbd_id Keyboard ID
633 */
634void ui_menu_press_accel(ui_menu_t *menu, char32_t c, sysarg_t kbd_id)
635{
636 if (menu->cb != NULL && menu->cb->press_accel != NULL)
637 menu->cb->press_accel(menu, menu->arg, c, kbd_id);
638}
639
[214aefb]640/** @}
641 */
Note: See TracBrowser for help on using the repository browser.