box.c (17354B)
1 /* 2 * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org> 3 * 4 * Permission to use, copy, modify, and/or distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 /* FIXME: implement other sticky options now supported by grid */ 18 19 #include <limits.h> 20 #include <string.h> 21 22 #include "box.h" 23 #include "event.h" 24 #include "graphics.h" 25 #include "memory.h" 26 #include "rect.h" 27 #include "scrollbar.h" 28 #include "widget.h" 29 30 static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip); 31 static void ltk_box_destroy(ltk_widget *self, int shallow); 32 static void ltk_recalculate_box(ltk_widget *self); 33 static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget); 34 static int ltk_box_remove_child(ltk_widget *self, ltk_widget *widget); 35 /* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow); */ 36 static int ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data); 37 static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event); 38 static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y); 39 static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r); 40 41 static ltk_widget *ltk_box_prev_child(ltk_widget *self, ltk_widget *child); 42 static ltk_widget *ltk_box_next_child(ltk_widget *self, ltk_widget *child); 43 static ltk_widget *ltk_box_first_child(ltk_widget *self); 44 static ltk_widget *ltk_box_last_child(ltk_widget *self); 45 46 static ltk_widget *ltk_box_nearest_child(ltk_widget *self, ltk_rect rect); 47 static ltk_widget *ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget); 48 static ltk_widget *ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget); 49 static ltk_widget *ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget); 50 static ltk_widget *ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget); 51 52 static void ltk_box_recalc_ideal_size(ltk_widget *self); 53 54 static struct ltk_widget_vtable vtable = { 55 .change_state = NULL, 56 .hide = NULL, 57 .draw = <k_box_draw, 58 .destroy = <k_box_destroy, 59 .resize = <k_recalculate_box, 60 .child_size_change = <k_box_child_size_change, 61 .remove_child = <k_box_remove_child, 62 .key_press = NULL, 63 .key_release = NULL, 64 .mouse_press = NULL, 65 .mouse_scroll = <k_box_mouse_scroll, 66 .mouse_release = NULL, 67 .motion_notify = NULL, 68 .get_child_at_pos = <k_box_get_child_at_pos, 69 .mouse_leave = NULL, 70 .mouse_enter = NULL, 71 .prev_child = <k_box_prev_child, 72 .next_child = <k_box_next_child, 73 .first_child = <k_box_first_child, 74 .last_child = <k_box_last_child, 75 .nearest_child = <k_box_nearest_child, 76 .nearest_child_left = <k_box_nearest_child_left, 77 .nearest_child_right = <k_box_nearest_child_right, 78 .nearest_child_above = <k_box_nearest_child_above, 79 .nearest_child_below = <k_box_nearest_child_below, 80 .ensure_rect_shown = <k_box_ensure_rect_shown, 81 .recalc_ideal_size = <k_box_recalc_ideal_size, 82 .type = LTK_WIDGET_BOX, 83 .flags = 0, 84 .invalid_signal = LTK_BOX_SIGNAL_INVALID, 85 }; 86 87 static void 88 ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) { 89 ltk_box *box = LTK_CAST_BOX(self); 90 ltk_widget *ptr; 91 /* FIXME: clip out scrollbar */ 92 ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip); 93 for (size_t i = 0; i < box->num_widgets; i++) { 94 ptr = box->widgets[i]; 95 /* FIXME: Maybe continue immediately if widget is 96 obviously outside of clipping rect */ 97 ltk_widget_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip)); 98 } 99 ltk_widget_draw( 100 LTK_CAST_WIDGET(box->sc), s, 101 x + box->sc->widget.lrect.x, 102 y + box->sc->widget.lrect.y, 103 ltk_rect_relative(box->sc->widget.lrect, real_clip) 104 ); 105 } 106 107 ltk_box * 108 ltk_box_create(ltk_window *window, ltk_orientation orient) { 109 ltk_box *box = ltk_malloc(sizeof(ltk_box)); 110 ltk_widget *self = LTK_CAST_WIDGET(box); 111 112 ltk_fill_widget_defaults(self, window, &vtable, 0, 0); 113 114 box->sc = ltk_scrollbar_create(window, orient); 115 box->sc->widget.parent = self; 116 ltk_widget_register_signal_handler( 117 LTK_CAST_WIDGET(box->sc), LTK_SCROLLBAR_SIGNAL_SCROLL, 118 <k_box_scroll_cb, LTK_MAKE_ARG_WIDGET(self) 119 ); 120 box->widgets = NULL; 121 box->num_alloc = 0; 122 box->num_widgets = 0; 123 box->orient = orient; 124 if (orient == LTK_HORIZONTAL) 125 box->widget.ideal_h = box->sc->widget.ideal_h; 126 else 127 box->widget.ideal_w = box->sc->widget.ideal_w; 128 ltk_recalculate_box(self); 129 130 return box; 131 } 132 133 static void 134 ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) { 135 ltk_box *box = LTK_CAST_BOX(self); 136 int delta = 0; 137 if (box->orient == LTK_HORIZONTAL) { 138 if (r.x + r.w > self->lrect.w && r.w <= self->lrect.w) 139 delta = r.x - (self->lrect.w - r.w); 140 else if (r.x < 0 || r.w > self->lrect.w) 141 delta = r.x; 142 } else { 143 if (r.y + r.h > self->lrect.h && r.h <= self->lrect.h) 144 delta = r.y - (self->lrect.h - r.h); 145 else if (r.y < 0 || r.h > self->lrect.h) 146 delta = r.y; 147 } 148 if (delta) 149 ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0); 150 } 151 152 static void 153 ltk_box_destroy(ltk_widget *self, int shallow) { 154 ltk_box *box = LTK_CAST_BOX(self); 155 ltk_widget *ptr; 156 for (size_t i = 0; i < box->num_widgets; i++) { 157 ptr = box->widgets[i]; 158 ptr->parent = NULL; 159 if (!shallow) 160 ltk_widget_destroy(ptr, shallow); 161 } 162 ltk_free(box->widgets); 163 box->sc->widget.parent = NULL; 164 ltk_widget_destroy(LTK_CAST_WIDGET(box->sc), 0); 165 ltk_free(box); 166 } 167 168 /* FIXME: Make this function name more consistent */ 169 /* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the 170 virtual_size is set - this can cause problems when a widget changes its size 171 (in the scrolled direction) when resized. */ 172 /* FIXME: avoid complete recalculation when just scrolling (only position updated) */ 173 static void 174 ltk_recalculate_box(ltk_widget *self) { 175 ltk_box *box = LTK_CAST_BOX(self); 176 ltk_widget *ptr; 177 ltk_rect *sc_rect = &box->sc->widget.lrect; 178 int cur_pos = 0; 179 if (box->orient == LTK_HORIZONTAL) 180 sc_rect->h = box->sc->widget.ideal_h; 181 else 182 sc_rect->w = box->sc->widget.ideal_w; 183 for (size_t i = 0; i < box->num_widgets; i++) { 184 ptr = box->widgets[i]; 185 ptr->lrect.w = ptr->ideal_w; 186 ptr->lrect.h = ptr->ideal_h; 187 if (box->orient == LTK_HORIZONTAL) { 188 ptr->lrect.x = cur_pos - box->sc->cur_pos; 189 if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM) 190 ptr->lrect.h = box->widget.lrect.h - sc_rect->h; 191 if (ptr->sticky & LTK_STICKY_TOP) 192 ptr->lrect.y = 0; 193 else if (ptr->sticky & LTK_STICKY_BOTTOM) 194 ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h; 195 else 196 ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2; 197 cur_pos += ptr->lrect.w; 198 } else { 199 ptr->lrect.y = cur_pos - box->sc->cur_pos; 200 if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT) 201 ptr->lrect.w = box->widget.lrect.w - sc_rect->w; 202 if (ptr->sticky & LTK_STICKY_LEFT) 203 ptr->lrect.x = 0; 204 else if (ptr->sticky & LTK_STICKY_RIGHT) 205 ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w; 206 else 207 ptr->lrect.x = (box->widget.lrect.w - ptr->lrect.w) / 2; 208 cur_pos += ptr->lrect.h; 209 } 210 ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect); 211 ltk_widget_resize(ptr); 212 } 213 ltk_scrollbar_set_virtual_size(box->sc, cur_pos); 214 if (box->orient == LTK_HORIZONTAL) { 215 sc_rect->x = 0; 216 sc_rect->y = box->widget.lrect.h - sc_rect->h; 217 sc_rect->w = box->widget.lrect.w; 218 } else { 219 sc_rect->x = box->widget.lrect.w - sc_rect->w; 220 sc_rect->y = 0; 221 sc_rect->h = box->widget.lrect.h; 222 } 223 *sc_rect = ltk_rect_intersect(*sc_rect, (ltk_rect){0, 0, box->widget.lrect.w, box->widget.lrect.h}); 224 box->sc->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, *sc_rect); 225 ltk_widget_resize(LTK_CAST_WIDGET(box->sc)); 226 } 227 228 static void 229 ltk_box_recalc_ideal_size(ltk_widget *self) { 230 ltk_box *box = LTK_CAST_BOX(self); 231 ltk_widget *ptr; 232 self->ideal_w = self->ideal_h = 0; 233 for (size_t i = 0; i < box->num_widgets; i++) { 234 ptr = box->widgets[i]; 235 ltk_widget_recalc_ideal_size(ptr); 236 if (box->orient == LTK_HORIZONTAL && ptr->ideal_h > self->ideal_h) { 237 self->ideal_h = ptr->ideal_h; 238 self->ideal_w += ptr->ideal_w; 239 } else if (box->orient == LTK_VERTICAL && ptr->ideal_w > self->ideal_w) { 240 self->ideal_w = ptr->ideal_w; 241 self->ideal_h += ptr->ideal_h; 242 } 243 } 244 ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(box->sc)); 245 if (box->orient == LTK_HORIZONTAL) 246 self->ideal_h += box->sc->widget.ideal_h; 247 else if (box->orient == LTK_VERTICAL) 248 self->ideal_w += box->sc->widget.ideal_w; 249 } 250 251 /* FIXME: This entire resizing thing is a bit weird. For instance, if a label 252 in a vertical box increases its height because its width has been decreased 253 and it is forced to wrap, should that just change the rect or also the 254 ideal size? Ideal size wouldn't really make sense here, but then the box 255 might be forced to add a scrollbar even though the parent widget would 256 actually give it more space if it knew that it needed it. */ 257 258 static void 259 ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) { 260 ltk_box *box = LTK_CAST_BOX(self); 261 short size_changed = 0; 262 /* This is always reset here - if it needs to be changed, 263 the resize function called by the last child_size_change 264 function will fix it */ 265 /* Note: This seems a bit weird, but if each widget set its rect itself, 266 that would also lead to weird things. For instance, if a butten is 267 added to a box after being ungridded, and its rect was changed 268 by the grid (e.g. because of a column weight), who should reset the 269 rect if it doesn't have sticky set? Of course, the resize function 270 could also set all widgets even if they don't have any sticky 271 settings, but there'd probably be some catch as well. */ 272 /* FIXME: the same comment as in grid.c applies */ 273 int orig_w = widget->lrect.w; 274 int orig_h = widget->lrect.h; 275 widget->lrect.w = widget->ideal_w; 276 widget->lrect.h = widget->ideal_h; 277 int sc_w = box->sc->widget.lrect.w; 278 int sc_h = box->sc->widget.lrect.h; 279 if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h > box->widget.ideal_h) { 280 box->widget.ideal_h = widget->ideal_h + sc_h; 281 size_changed = 1; 282 } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w > box->widget.ideal_h) { 283 box->widget.ideal_w = widget->ideal_w + sc_w; 284 size_changed = 1; 285 } 286 287 if (size_changed && box->widget.parent && box->widget.parent->vtable->child_size_change) 288 box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box); 289 else 290 ltk_recalculate_box(LTK_CAST_WIDGET(box)); 291 if (orig_w != widget->lrect.w || orig_h != widget->lrect.h) 292 ltk_widget_resize(widget); 293 } 294 295 int 296 ltk_box_add(ltk_box *box, ltk_widget *widget, ltk_sticky_mask sticky) { 297 if (widget->parent) 298 return 1; 299 if (box->num_widgets >= box->num_alloc) { 300 size_t new_size = box->num_alloc > 0 ? box->num_alloc * 2 : 4; 301 ltk_widget **new = ltk_realloc(box->widgets, new_size * sizeof(ltk_widget *)); 302 box->num_alloc = new_size; 303 box->widgets = new; 304 } 305 ltk_widget_recalc_ideal_size(widget); 306 307 int sc_w = box->sc->widget.lrect.w; 308 int sc_h = box->sc->widget.lrect.h; 309 310 box->widgets[box->num_widgets++] = widget; 311 if (box->orient == LTK_HORIZONTAL) { 312 box->widget.ideal_w += widget->ideal_w; 313 if (widget->ideal_h + sc_h > box->widget.ideal_h) 314 box->widget.ideal_h = widget->ideal_h + sc_h; 315 } else { 316 box->widget.ideal_h += widget->ideal_h; 317 if (widget->ideal_w + sc_w > box->widget.ideal_w) 318 box->widget.ideal_w = widget->ideal_w + sc_w; 319 } 320 widget->parent = LTK_CAST_WIDGET(box); 321 widget->sticky = sticky; 322 ltk_box_child_size_change(LTK_CAST_WIDGET(box), widget); 323 ltk_window_invalidate_widget_rect(box->widget.window, LTK_CAST_WIDGET(box)); 324 325 return 0; 326 } 327 328 int 329 ltk_box_remove_index(ltk_box *box, size_t index) { 330 if (index >= box->num_widgets) 331 return 1; 332 ltk_widget *self = LTK_CAST_WIDGET(box); 333 ltk_widget *widget = box->widgets[index]; 334 int sc_w = box->sc->widget.lrect.w; 335 int sc_h = box->sc->widget.lrect.h; 336 if (index < box->num_widgets - 1) 337 memmove(box->widgets + index, box->widgets + index + 1, 338 (box->num_widgets - index - 1) * sizeof(ltk_widget *)); 339 box->num_widgets--; 340 ltk_window_invalidate_widget_rect(self->window, self); 341 /* search for new ideal width/height */ 342 /* FIXME: make this all a bit nicer and break the lines better */ 343 /* FIXME: other part of ideal size not updated */ 344 if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == self->ideal_h) { 345 self->ideal_h = 0; 346 for (size_t j = 0; j < box->num_widgets; j++) { 347 if (box->widgets[j]->ideal_h + sc_h > self->ideal_h) 348 self->ideal_h = box->widgets[j]->ideal_h + sc_h; 349 } 350 if (self->parent) 351 ltk_widget_resize(self->parent); 352 } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == self->ideal_w) { 353 self->ideal_w = 0; 354 for (size_t j = 0; j < box->num_widgets; j++) { 355 if (box->widgets[j]->ideal_w + sc_w > self->ideal_w) 356 self->ideal_w = box->widgets[j]->ideal_w + sc_w; 357 } 358 if (self->parent) 359 ltk_widget_resize(self->parent); 360 } 361 return 0; 362 } 363 364 int 365 ltk_box_remove(ltk_box *box, ltk_widget *widget) { 366 if (widget->parent != LTK_CAST_WIDGET(box)) 367 return 1; 368 widget->parent = NULL; 369 for (size_t i = 0; i < box->num_widgets; i++) { 370 if (box->widgets[i] == widget) { 371 return ltk_box_remove_index(box, i); 372 } 373 } 374 375 return 1; 376 } 377 378 static int 379 ltk_box_remove_child(ltk_widget *self, ltk_widget *widget) { 380 return ltk_box_remove(LTK_CAST_BOX(self), widget); 381 } 382 383 /* FIXME: maybe come up with a more efficient method */ 384 static ltk_widget * 385 ltk_box_nearest_child(ltk_widget *self, ltk_rect rect) { 386 ltk_box *box = LTK_CAST_BOX(self); 387 ltk_widget *minw = NULL; 388 int min_dist = INT_MAX; 389 for (size_t i = 0; i < box->num_widgets; i++) { 390 ltk_rect r = box->widgets[i]->lrect; 391 int dist = ltk_rect_fakedist(rect, r); 392 if (dist < min_dist) { 393 min_dist = dist; 394 minw = box->widgets[i]; 395 } 396 } 397 return minw; 398 } 399 400 static ltk_widget * 401 ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget) { 402 ltk_box *box = LTK_CAST_BOX(self); 403 if (box->orient == LTK_VERTICAL) 404 return NULL; 405 return ltk_box_prev_child(self, widget); 406 } 407 408 static ltk_widget * 409 ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget) { 410 ltk_box *box = LTK_CAST_BOX(self); 411 if (box->orient == LTK_VERTICAL) 412 return NULL; 413 return ltk_box_next_child(self, widget); 414 } 415 416 static ltk_widget * 417 ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget) { 418 ltk_box *box = LTK_CAST_BOX(self); 419 if (box->orient == LTK_HORIZONTAL) 420 return NULL; 421 return ltk_box_prev_child(self, widget); 422 } 423 424 static ltk_widget * 425 ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget) { 426 ltk_box *box = LTK_CAST_BOX(self); 427 if (box->orient == LTK_HORIZONTAL) 428 return NULL; 429 return ltk_box_next_child(self, widget); 430 } 431 432 static ltk_widget * 433 ltk_box_prev_child(ltk_widget *self, ltk_widget *child) { 434 ltk_box *box = LTK_CAST_BOX(self); 435 for (size_t i = box->num_widgets; i-- > 0;) { 436 if (box->widgets[i] == child) 437 return i > 0 ? box->widgets[i-1] : NULL; 438 } 439 return NULL; 440 } 441 442 static ltk_widget * 443 ltk_box_next_child(ltk_widget *self, ltk_widget *child) { 444 ltk_box *box = LTK_CAST_BOX(self); 445 for (size_t i = 0; i < box->num_widgets; i++) { 446 if (box->widgets[i] == child) 447 return i < box->num_widgets - 1 ? box->widgets[i+1] : NULL; 448 } 449 return NULL; 450 } 451 452 static ltk_widget * 453 ltk_box_first_child(ltk_widget *self) { 454 ltk_box *box = LTK_CAST_BOX(self); 455 return box->num_widgets > 0 ? box->widgets[0] : NULL; 456 } 457 458 static ltk_widget * 459 ltk_box_last_child(ltk_widget *self) { 460 ltk_box *box = LTK_CAST_BOX(self); 461 return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL; 462 } 463 464 static int 465 ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) { 466 (void)self; 467 (void)args; 468 ltk_widget *boxw = LTK_CAST_ARG_WIDGET(data); 469 ltk_recalculate_box(boxw); 470 ltk_window_invalidate_widget_rect(boxw->window, boxw); 471 return 1; 472 } 473 474 static ltk_widget * 475 ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) { 476 ltk_box *box = LTK_CAST_BOX(self); 477 if (ltk_collide_rect(box->sc->widget.crect, x, y)) 478 return (ltk_widget *)box->sc; 479 for (size_t i = 0; i < box->num_widgets; i++) { 480 if (ltk_collide_rect(box->widgets[i]->crect, x, y)) 481 return box->widgets[i]; 482 } 483 return NULL; 484 } 485 486 static int 487 ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) { 488 ltk_box *box = LTK_CAST_BOX(self); 489 if (event->dy) { 490 /* FIXME: horizontal scrolling, etc. */ 491 /* FIXME: configure scrollstep */ 492 int delta = event->dy * -15; 493 ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0); 494 ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y); 495 ltk_window_fake_motion_event(self->window, glob.x, glob.y); 496 return 1; 497 } 498 return 0; 499 }