ltk

Socket-based GUI for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

box.c (19290B)


      1 /*
      2  * Copyright (c) 2021-2023 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 <stdio.h>
     20 #include <stdlib.h>
     21 #include <stdarg.h>
     22 #include <stdint.h>
     23 #include <string.h>
     24 
     25 #include "event.h"
     26 #include "memory.h"
     27 #include "color.h"
     28 #include "rect.h"
     29 #include "widget.h"
     30 #include "ltk.h"
     31 #include "util.h"
     32 #include "scrollbar.h"
     33 #include "box.h"
     34 #include "cmd.h"
     35 
     36 static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
     37 static ltk_box *ltk_box_create(ltk_window *window, const char *id, ltk_orientation orient);
     38 static void ltk_box_destroy(ltk_widget *self, int shallow);
     39 static void ltk_recalculate_box(ltk_widget *self);
     40 static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
     41 static int ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, ltk_sticky_mask sticky, ltk_error *err);
     42 static int ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err);
     43 /* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow, ltk_error *err); */
     44 static void ltk_box_scroll(ltk_widget *self);
     45 static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
     46 static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
     47 static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
     48 
     49 static ltk_widget *ltk_box_prev_child(ltk_widget *self, ltk_widget *child);
     50 static ltk_widget *ltk_box_next_child(ltk_widget *self, ltk_widget *child);
     51 static ltk_widget *ltk_box_first_child(ltk_widget *self);
     52 static ltk_widget *ltk_box_last_child(ltk_widget *self);
     53 
     54 static ltk_widget *ltk_box_nearest_child(ltk_widget *self, ltk_rect rect);
     55 static ltk_widget *ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget);
     56 static ltk_widget *ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget);
     57 static ltk_widget *ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget);
     58 static ltk_widget *ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget);
     59 
     60 static struct ltk_widget_vtable vtable = {
     61 	.change_state = NULL,
     62 	.hide = NULL,
     63 	.draw = &ltk_box_draw,
     64 	.destroy = &ltk_box_destroy,
     65 	.resize = &ltk_recalculate_box,
     66 	.child_size_change = &ltk_box_child_size_change,
     67 	.remove_child = &ltk_box_remove,
     68 	.key_press = NULL,
     69 	.key_release = NULL,
     70 	.mouse_press = NULL,
     71 	.mouse_scroll = &ltk_box_mouse_scroll,
     72 	.mouse_release = NULL,
     73 	.motion_notify = NULL,
     74 	.get_child_at_pos = &ltk_box_get_child_at_pos,
     75 	.mouse_leave = NULL,
     76 	.mouse_enter = NULL,
     77 	.prev_child = &ltk_box_prev_child,
     78 	.next_child = &ltk_box_next_child,
     79 	.first_child = &ltk_box_first_child,
     80 	.last_child = &ltk_box_last_child,
     81 	.nearest_child = &ltk_box_nearest_child,
     82 	.nearest_child_left = &ltk_box_nearest_child_left,
     83 	.nearest_child_right = &ltk_box_nearest_child_right,
     84 	.nearest_child_above = &ltk_box_nearest_child_above,
     85 	.nearest_child_below = &ltk_box_nearest_child_below,
     86 	.ensure_rect_shown = &ltk_box_ensure_rect_shown,
     87 	.type = LTK_WIDGET_BOX,
     88 	.flags = 0,
     89 };
     90 
     91 static int ltk_box_cmd_add(
     92     ltk_window *window,
     93     ltk_box *box,
     94     ltk_cmd_token *tokens,
     95     size_t num_tokens,
     96     ltk_error *err);
     97 static int ltk_box_cmd_remove(
     98     ltk_window *window,
     99     ltk_box *box,
    100     ltk_cmd_token *tokens,
    101     size_t num_tokens,
    102     ltk_error *err);
    103 /*
    104 static int ltk_box_cmd_clear
    105     ltk_window *window,
    106     ltk_cmd_token *tokens,
    107     size_t num_tokens,
    108     ltk_error *err);
    109 */
    110 static int ltk_box_cmd_create(
    111     ltk_window *window,
    112     ltk_box *box,
    113     ltk_cmd_token *tokens,
    114     size_t num_tokens,
    115     ltk_error *err);
    116 
    117 static void
    118 ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
    119 	ltk_box *box = (ltk_box *)self;
    120 	ltk_widget *ptr;
    121 	/* FIXME: clip out scrollbar */
    122 	ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
    123 	for (size_t i = 0; i < box->num_widgets; i++) {
    124 		ptr = box->widgets[i];
    125 		/* FIXME: Maybe continue immediately if widget is
    126 		   obviously outside of clipping rect */
    127 		ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
    128 	}
    129 	box->sc->widget.vtable->draw(
    130 	    (ltk_widget *)box->sc, s,
    131 	    x + box->sc->widget.lrect.x,
    132 	    y + box->sc->widget.lrect.y,
    133 	    ltk_rect_relative(box->sc->widget.lrect, real_clip)
    134 	);
    135 }
    136 
    137 static ltk_box *
    138 ltk_box_create(ltk_window *window, const char *id, ltk_orientation orient) {
    139 	ltk_box *box = ltk_malloc(sizeof(ltk_box));
    140 
    141 	ltk_fill_widget_defaults(&box->widget, id, window, &vtable, 0, 0);
    142 
    143 	box->sc = ltk_scrollbar_create(window, orient, &ltk_box_scroll, box);
    144 	box->sc->widget.parent = &box->widget;
    145 	box->widgets = NULL;
    146 	box->num_alloc = 0;
    147 	box->num_widgets = 0;
    148 	box->orient = orient;
    149 	if (orient == LTK_HORIZONTAL)
    150 		box->widget.ideal_h = box->sc->widget.ideal_h;
    151 	else
    152 		box->widget.ideal_w = box->sc->widget.ideal_w;
    153 	ltk_recalculate_box(&box->widget);
    154 
    155 	return box;
    156 }
    157 
    158 static void
    159 ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
    160 	ltk_box *box = (ltk_box *)self;
    161 	int delta = 0;
    162 	if (box->orient == LTK_HORIZONTAL) {
    163 		if (r.x + r.w > self->lrect.w && r.w <= self->lrect.w)
    164 			delta = r.x - (self->lrect.w - r.w);
    165 		else if (r.x < 0 || r.w > self->lrect.w)
    166 			delta = r.x;
    167 	} else {
    168 		if (r.y + r.h > self->lrect.h && r.h <= self->lrect.h)
    169 			delta = r.y - (self->lrect.h - r.h);
    170 		else if (r.y < 0 || r.h > self->lrect.h)
    171 			delta = r.y;
    172 	}
    173 	if (delta)
    174 		ltk_scrollbar_scroll(&box->sc->widget, delta, 0);
    175 }
    176 
    177 static void
    178 ltk_box_destroy(ltk_widget *self, int shallow) {
    179 	ltk_box *box = (ltk_box *)self;
    180 	ltk_widget *ptr;
    181 	ltk_error err;
    182 	for (size_t i = 0; i < box->num_widgets; i++) {
    183 		ptr = box->widgets[i];
    184 		ptr->parent = NULL;
    185 		if (!shallow)
    186 			ltk_widget_destroy(ptr, shallow, &err);
    187 	}
    188 	ltk_free(box->widgets);
    189 	box->sc->widget.parent = NULL;
    190 	/* FIXME: this is a bit weird because sc isn't a normal widget
    191 	   (it isn't in the widget hash) */
    192 	ltk_widget_destroy(&box->sc->widget, 0, &err);
    193 	ltk_free(box);
    194 }
    195 
    196 /* FIXME: Make this function name more consistent */
    197 /* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
    198    virtual_size is set - this can cause problems when a widget changes its size
    199    (in the scrolled direction) when resized. */
    200 /* FIXME: avoid complete recalculation when just scrolling (only position updated) */
    201 static void
    202 ltk_recalculate_box(ltk_widget *self) {
    203 	ltk_box *box = (ltk_box *)self;
    204 	ltk_widget *ptr;
    205 	ltk_rect *sc_rect = &box->sc->widget.lrect;
    206 	int cur_pos = 0;
    207 	if (box->orient == LTK_HORIZONTAL)
    208 		sc_rect->h = box->sc->widget.ideal_h;
    209 	else
    210 		sc_rect->w = box->sc->widget.ideal_w;
    211 	for (size_t i = 0; i < box->num_widgets; i++) {
    212 		ptr = box->widgets[i];
    213 		if (box->orient == LTK_HORIZONTAL) {
    214 			ptr->lrect.x = cur_pos - box->sc->cur_pos;
    215 			if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
    216 				ptr->lrect.h = box->widget.lrect.h - sc_rect->h;
    217 			if (ptr->sticky & LTK_STICKY_TOP)
    218 				ptr->lrect.y = 0;
    219 			else if (ptr->sticky & LTK_STICKY_BOTTOM)
    220 				ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h;
    221 			else
    222 				ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2;
    223 			cur_pos += ptr->lrect.w;
    224 		} else {
    225 			ptr->lrect.y = cur_pos - box->sc->cur_pos;
    226 			if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
    227 				ptr->lrect.w = box->widget.lrect.w - sc_rect->w;
    228 			if (ptr->sticky & LTK_STICKY_LEFT)
    229 				ptr->lrect.x = 0;
    230 			else if (ptr->sticky & LTK_STICKY_RIGHT)
    231 				ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w;
    232 			else
    233 				ptr->lrect.x = (box->widget.lrect.w - ptr->lrect.w) / 2;
    234 			cur_pos += ptr->lrect.h;
    235 		}
    236 		ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
    237 		ltk_widget_resize(ptr);
    238 	}
    239 	ltk_scrollbar_set_virtual_size(box->sc, cur_pos);
    240 	if (box->orient == LTK_HORIZONTAL) {
    241 		sc_rect->x = 0;
    242 		sc_rect->y = box->widget.lrect.h - sc_rect->h;
    243 		sc_rect->w = box->widget.lrect.w;
    244 	} else {
    245 		sc_rect->x = box->widget.lrect.w - sc_rect->w;
    246 		sc_rect->y = 0;
    247 		sc_rect->h = box->widget.lrect.h;
    248 	}
    249 	*sc_rect = ltk_rect_intersect(*sc_rect, (ltk_rect){0, 0, box->widget.lrect.w, box->widget.lrect.h});
    250 	box->sc->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, *sc_rect);
    251 	ltk_widget_resize((ltk_widget *)box->sc);
    252 }
    253 
    254 /* FIXME: This entire resizing thing is a bit weird. For instance, if a label
    255    in a vertical box increases its height because its width has been decreased
    256    and it is forced to wrap, should that just change the rect or also the
    257    ideal size? Ideal size wouldn't really make sense here, but then the box
    258    might be forced to add a scrollbar even though the parent widget would
    259    actually give it more space if it knew that it needed it. */
    260 
    261 static void
    262 ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
    263 	ltk_box *box = (ltk_box *)self;
    264 	short size_changed = 0;
    265 	/* This is always reset here - if it needs to be changed,
    266 	   the resize function called by the last child_size_change
    267 	   function will fix it */
    268 	/* Note: This seems a bit weird, but if each widget set its rect itself,
    269 	   that would also lead to weird things. For instance, if a butten is
    270 	   added to after a box after being ungridded, and its rect was changed
    271 	   by the grid (e.g. because of a column weight), who should reset the
    272 	   rect if it doesn't have sticky set? Of course, the resize function
    273 	   could also set all widgets even if they don't have any sticky
    274 	   settings, but there'd probably be some catch as well. */
    275 	/* FIXME: the same comment as in grid.c applies */
    276 	int orig_w = widget->lrect.w;
    277 	int orig_h = widget->lrect.h;
    278 	widget->lrect.w = widget->ideal_w;
    279 	widget->lrect.h = widget->ideal_h;
    280 	int sc_w = box->sc->widget.lrect.w;
    281 	int sc_h = box->sc->widget.lrect.h;
    282 	if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h > box->widget.ideal_h) {
    283 		box->widget.ideal_h = widget->ideal_h + sc_h;
    284 		size_changed = 1;
    285 	} else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w > box->widget.ideal_h) {
    286 		box->widget.ideal_w = widget->ideal_w + sc_w;
    287 		size_changed = 1;
    288 	}
    289 
    290 	if (size_changed && box->widget.parent && box->widget.parent->vtable->child_size_change)
    291 		box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box);
    292 	else
    293 		ltk_recalculate_box((ltk_widget *)box);
    294 	if (orig_w != widget->lrect.w || orig_h != widget->lrect.h)
    295 		ltk_widget_resize(widget);
    296 }
    297 
    298 static int
    299 ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, ltk_sticky_mask sticky, ltk_error *err) {
    300 	if (widget->parent) {
    301 		err->type = ERR_WIDGET_IN_CONTAINER;
    302 		return 1;
    303 	}
    304 	if (box->num_widgets >= box->num_alloc) {
    305 		size_t new_size = box->num_alloc > 0 ? box->num_alloc * 2 : 4;
    306 		ltk_widget **new = ltk_realloc(box->widgets, new_size * sizeof(ltk_widget *));
    307 		box->num_alloc = new_size;
    308 		box->widgets = new;
    309 	}
    310 
    311 	int sc_w = box->sc->widget.lrect.w;
    312 	int sc_h = box->sc->widget.lrect.h;
    313 
    314 	box->widgets[box->num_widgets++] = widget;
    315 	if (box->orient == LTK_HORIZONTAL) {
    316 		box->widget.ideal_w += widget->ideal_w;
    317 		if (widget->ideal_h + sc_h > box->widget.ideal_h)
    318 			box->widget.ideal_h = widget->ideal_h + sc_h;
    319 	} else {
    320 		box->widget.ideal_h += widget->ideal_h;
    321 		if (widget->ideal_w + sc_w > box->widget.ideal_w)
    322 			box->widget.ideal_w = widget->ideal_w + sc_w;
    323 	}
    324 	widget->parent = (ltk_widget *)box;
    325 	widget->sticky = sticky;
    326 	ltk_box_child_size_change((ltk_widget *)box, widget);
    327 	ltk_window_invalidate_widget_rect(window, &box->widget);
    328 
    329 	return 0;
    330 }
    331 
    332 static int
    333 ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
    334 	ltk_box *box = (ltk_box *)self;
    335 	int sc_w = box->sc->widget.lrect.w;
    336 	int sc_h = box->sc->widget.lrect.h;
    337 	if (widget->parent != (ltk_widget *)box) {
    338 		err->type = ERR_WIDGET_NOT_IN_CONTAINER;
    339 		return 1;
    340 	}
    341 	widget->parent = NULL;
    342 	for (size_t i = 0; i < box->num_widgets; i++) {
    343 		if (box->widgets[i] == widget) {
    344 			if (i < box->num_widgets - 1)
    345 				memmove(box->widgets + i, box->widgets + i + 1,
    346 				    (box->num_widgets - i - 1) * sizeof(ltk_widget *));
    347 			box->num_widgets--;
    348 			ltk_window_invalidate_widget_rect(widget->window, &box->widget);
    349 			/* search for new ideal width/height */
    350 			/* FIXME: make this all a bit nicer and break the lines better */
    351 			/* FIXME: other part of ideal size not updated */
    352 			if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == box->widget.ideal_h) {
    353 				box->widget.ideal_h = 0;
    354 				for (size_t j = 0; j < box->num_widgets; j++) {
    355 					if (box->widgets[j]->ideal_h + sc_h > box->widget.ideal_h)
    356 						box->widget.ideal_h = box->widgets[j]->ideal_h + sc_h;
    357 				}
    358 				if (box->widget.parent)
    359 					ltk_widget_resize(box->widget.parent);
    360 			} else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == box->widget.ideal_w) {
    361 				box->widget.ideal_w = 0;
    362 				for (size_t j = 0; j < box->num_widgets; j++) {
    363 					if (box->widgets[j]->ideal_w + sc_w > box->widget.ideal_w)
    364 						box->widget.ideal_w = box->widgets[j]->ideal_w + sc_w;
    365 				}
    366 				if (box->widget.parent)
    367 					ltk_widget_resize(box->widget.parent);
    368 			}
    369 			return 0;
    370 		}
    371 	}
    372 
    373 	err->type = ERR_WIDGET_NOT_IN_CONTAINER;
    374 	return 1;
    375 }
    376 
    377 /* FIXME: maybe come up with a more efficient method */
    378 static ltk_widget *
    379 ltk_box_nearest_child(ltk_widget *self, ltk_rect rect) {
    380 	ltk_box *box = (ltk_box *)self;
    381 	ltk_widget *minw = NULL;
    382 	int min_dist = INT_MAX;
    383 	int cx = rect.x + rect.w / 2;
    384 	int cy = rect.y + rect.h / 2;
    385 	ltk_rect r;
    386 	int dist;
    387 	for (size_t i = 0; i < box->num_widgets; i++) {
    388 		r = box->widgets[i]->lrect;
    389 		dist = abs((r.x + r.w / 2) - cx) + abs((r.y + r.h / 2) - cy);
    390 		if (dist < min_dist) {
    391 			min_dist = dist;
    392 			minw = box->widgets[i];
    393 		}
    394 	}
    395 	return minw;
    396 }
    397 
    398 static ltk_widget *
    399 ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
    400 	ltk_box *box = (ltk_box *)self;
    401 	if (box->orient == LTK_VERTICAL)
    402 		return NULL;
    403 	return ltk_box_prev_child(self, widget);
    404 }
    405 
    406 static ltk_widget *
    407 ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
    408 	ltk_box *box = (ltk_box *)self;
    409 	if (box->orient == LTK_VERTICAL)
    410 		return NULL;
    411 	return ltk_box_next_child(self, widget);
    412 }
    413 
    414 static ltk_widget *
    415 ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
    416 	ltk_box *box = (ltk_box *)self;
    417 	if (box->orient == LTK_HORIZONTAL)
    418 		return NULL;
    419 	return ltk_box_prev_child(self, widget);
    420 }
    421 
    422 static ltk_widget *
    423 ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
    424 	ltk_box *box = (ltk_box *)self;
    425 	if (box->orient == LTK_HORIZONTAL)
    426 		return NULL;
    427 	return ltk_box_next_child(self, widget);
    428 }
    429 
    430 static ltk_widget *
    431 ltk_box_prev_child(ltk_widget *self, ltk_widget *child) {
    432 	ltk_box *box = (ltk_box *)self;
    433 	for (size_t i = box->num_widgets; i-- > 0;) {
    434 		if (box->widgets[i] == child)
    435 			return i > 0 ? box->widgets[i-1] : NULL;
    436 	}
    437 	return NULL;
    438 }
    439 
    440 static ltk_widget *
    441 ltk_box_next_child(ltk_widget *self, ltk_widget *child) {
    442 	ltk_box *box = (ltk_box *)self;
    443 	for (size_t i = 0; i < box->num_widgets; i++) {
    444 		if (box->widgets[i] == child)
    445 			return i < box->num_widgets - 1 ? box->widgets[i+1] : NULL;
    446 	}
    447 	return NULL;
    448 }
    449 
    450 static ltk_widget *
    451 ltk_box_first_child(ltk_widget *self) {
    452 	ltk_box *box = (ltk_box *)self;
    453 	return box->num_widgets > 0 ? box->widgets[0] : NULL;
    454 }
    455 
    456 static ltk_widget *
    457 ltk_box_last_child(ltk_widget *self) {
    458 	ltk_box *box = (ltk_box *)self;
    459 	return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL;
    460 }
    461 
    462 static void
    463 ltk_box_scroll(ltk_widget *self) {
    464 	ltk_box *box = (ltk_box *)self;
    465 	ltk_recalculate_box(self);
    466 	ltk_window_invalidate_widget_rect(box->widget.window, &box->widget);
    467 }
    468 
    469 static ltk_widget *
    470 ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
    471 	ltk_box *box = (ltk_box *)self;
    472 	if (ltk_collide_rect(box->sc->widget.crect, x, y))
    473 		return (ltk_widget *)box->sc;
    474 	for (size_t i = 0; i < box->num_widgets; i++) {
    475 		if (ltk_collide_rect(box->widgets[i]->crect, x, y))
    476 			return box->widgets[i];
    477 	}
    478 	return NULL;
    479 }
    480 
    481 static int
    482 ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
    483 	ltk_box *box = (ltk_box *)self;
    484 	if (event->dy) {
    485 		/* FIXME: horizontal scrolling, etc. */
    486 		/* FIXME: configure scrollstep */
    487 		int delta = event->dy * -15;
    488 		ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
    489 		ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
    490 		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
    491 		return 1;
    492 	}
    493 	return 0;
    494 }
    495 
    496 /* box <box id> add <widget id> [sticky] */
    497 static int
    498 ltk_box_cmd_add(
    499     ltk_window *window,
    500     ltk_box *box,
    501     ltk_cmd_token *tokens,
    502     size_t num_tokens,
    503     ltk_error *err) {
    504 	ltk_cmdarg_parseinfo cmd[] = {
    505 		{.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_ANY, .optional = 0},
    506 		{.type = CMDARG_STICKY, .optional = 1},
    507 	};
    508 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
    509 		return 1;
    510 	if (ltk_box_add(window, cmd[0].val.widget, box, cmd[1].initialized ? cmd[1].val.sticky : 0, err)) {
    511 		err->arg = 0;
    512 		return 1;
    513 	}
    514 	return 0;
    515 }
    516 
    517 /* box <box id> remove <widget id> */
    518 static int
    519 ltk_box_cmd_remove(
    520     ltk_window *window,
    521     ltk_box *box,
    522     ltk_cmd_token *tokens,
    523     size_t num_tokens,
    524     ltk_error *err) {
    525 	ltk_cmdarg_parseinfo cmd[] = {
    526 		{.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_ANY, .optional = 0},
    527 	};
    528 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
    529 		return 1;
    530 	if (ltk_box_remove(cmd[0].val.widget, (ltk_widget *)box, err)) {
    531 		err->arg = 0;
    532 		return 1;
    533 	}
    534 	return 0;
    535 }
    536 
    537 /* box <box id> create <orientation> */
    538 static int
    539 ltk_box_cmd_create(
    540     ltk_window *window,
    541     ltk_box *box_unneeded,
    542     ltk_cmd_token *tokens,
    543     size_t num_tokens,
    544     ltk_error *err) {
    545 	(void)box_unneeded;
    546 	ltk_cmdarg_parseinfo cmd[] = {
    547 		{.type = CMDARG_IGNORE, .optional = 0},
    548 		{.type = CMDARG_STRING, .optional = 0},
    549 		{.type = CMDARG_IGNORE, .optional = 0},
    550 		{.type = CMDARG_ORIENTATION, .optional = 0},
    551 	};
    552 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
    553 		return 1;
    554 	if (!ltk_widget_id_free(cmd[1].val.str)) {
    555 		err->type = ERR_WIDGET_ID_IN_USE;
    556 		err->arg = 1;
    557 		return 1;
    558 	}
    559 	ltk_box *box = ltk_box_create(window, cmd[1].val.str, cmd[3].val.orient);
    560 	ltk_set_widget((ltk_widget *)box, cmd[1].val.str);
    561 
    562 	return 0;
    563 }
    564 
    565 static struct box_cmd {
    566 	char *name;
    567 	int (*func)(ltk_window *, ltk_box *, ltk_cmd_token *, size_t, ltk_error *);
    568 	int needs_all;
    569 } box_cmds[] = {
    570         {"add", &ltk_box_cmd_add, 0},
    571         {"create", &ltk_box_cmd_create, 1},
    572         {"remove", &ltk_box_cmd_remove, 0},
    573 };
    574 
    575 GEN_CMD_HELPERS(ltk_box_cmd, LTK_WIDGET_BOX, ltk_box, box_cmds, struct box_cmd)