ltk

GUI toolkit 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)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
Log | Files | Refs | README | LICENSE

box.c (19389B)


      1 /*
      2  * Copyright (c) 2021-2026 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 #include "ltk.h"
     30 
     31 static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
     32 static void ltk_box_destroy(ltk_widget *self, int shallow);
     33 static void ltk_recalculate_box(ltk_widget *self);
     34 static void ltk_box_child_size_change(ltk_widget *self, ltk_widget_id childid);
     35 static int ltk_box_remove_child(ltk_widget *self, ltk_widget_id widgetid);
     36 /* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow); */
     37 static int ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data);
     38 static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
     39 static ltk_widget_id ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
     40 static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
     41 
     42 static ltk_widget_id ltk_box_prev_child(ltk_widget *self, ltk_widget_id childid);
     43 static ltk_widget_id ltk_box_next_child(ltk_widget *self, ltk_widget_id childid);
     44 static ltk_widget_id ltk_box_first_child(ltk_widget *self);
     45 static ltk_widget_id ltk_box_last_child(ltk_widget *self);
     46 
     47 static ltk_widget_id ltk_box_nearest_child(ltk_widget *self, ltk_rect rect);
     48 static ltk_widget_id ltk_box_nearest_child_left(ltk_widget *self, ltk_widget_id childid);
     49 static ltk_widget_id ltk_box_nearest_child_right(ltk_widget *self, ltk_widget_id childid);
     50 static ltk_widget_id ltk_box_nearest_child_above(ltk_widget *self, ltk_widget_id childid);
     51 static ltk_widget_id ltk_box_nearest_child_below(ltk_widget *self, ltk_widget_id childid);
     52 
     53 static void ltk_box_recalc_ideal_size(ltk_widget *self);
     54 
     55 static struct ltk_widget_vtable vtable = {
     56 	.change_state = NULL,
     57 	.hide = NULL,
     58 	.draw = &ltk_box_draw,
     59 	.destroy = &ltk_box_destroy,
     60 	.resize = &ltk_recalculate_box,
     61 	.child_size_change = &ltk_box_child_size_change,
     62 	.remove_child = &ltk_box_remove_child,
     63 	.key_press = NULL,
     64 	.key_release = NULL,
     65 	.mouse_press = NULL,
     66 	.mouse_scroll = &ltk_box_mouse_scroll,
     67 	.mouse_release = NULL,
     68 	.motion_notify = NULL,
     69 	.get_child_at_pos = &ltk_box_get_child_at_pos,
     70 	.mouse_leave = NULL,
     71 	.mouse_enter = NULL,
     72 	.prev_child = &ltk_box_prev_child,
     73 	.next_child = &ltk_box_next_child,
     74 	.first_child = &ltk_box_first_child,
     75 	.last_child = &ltk_box_last_child,
     76 	.nearest_child = &ltk_box_nearest_child,
     77 	.nearest_child_left = &ltk_box_nearest_child_left,
     78 	.nearest_child_right = &ltk_box_nearest_child_right,
     79 	.nearest_child_above = &ltk_box_nearest_child_above,
     80 	.nearest_child_below = &ltk_box_nearest_child_below,
     81 	.ensure_rect_shown = &ltk_box_ensure_rect_shown,
     82 	.recalc_ideal_size = &ltk_box_recalc_ideal_size,
     83 	.type = LTK_WIDGET_BOX,
     84 	.flags = 0,
     85 	.invalid_signal = LTK_BOX_SIGNAL_INVALID,
     86 };
     87 
     88 static void
     89 ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
     90 	ltk_box *box = LTK_CAST_BOX(self);
     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 < ltk_array_len(box->widgets); i++) {
     94 		ltk_widget *ptr = ltk_get_widget_from_id(ltk_array_get(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 *scrollbar = ltk_get_widget_from_id(box->scrollbar);
    100 	ltk_widget_draw(
    101 	    scrollbar, s,
    102 	    x + scrollbar->lrect.x,
    103 	    y + scrollbar->lrect.y,
    104 	    ltk_rect_relative(scrollbar->lrect, real_clip)
    105 	);
    106 }
    107 
    108 ltk_widget_id
    109 ltk_box_create(ltk_widget_id windowid, ltk_orientation orient) {
    110 	ltk_box *box = ltk_malloc(sizeof(ltk_box));
    111 	ltk_widget *self = LTK_CAST_WIDGET(box);
    112 
    113 	ltk_widget_id id = ltk_initialize_widget(self, windowid, &vtable, 0, 0);
    114 
    115 	box->scrollbar = ltk_scrollbar_create(windowid, orient);
    116 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    117 	scwidget->parent = id;
    118 	/* FIXM: separate callback function that takes widget ID that can be used safely externally
    119 	   (e.g. if an independent scrollbar should be associated with the box) */
    120 	ltk_widget_register_signal_handler(
    121 		box->scrollbar, LTK_SCROLLBAR_SIGNAL_SCROLL,
    122 		&ltk_box_scroll_cb, LTK_MAKE_ARG_WIDGET_ID(self->id)
    123 	);
    124 	box->widgets = ltk_array_create(widget_id, 1);
    125 	box->orient = orient;
    126 	if (orient == LTK_HORIZONTAL)
    127 		self->ideal_h = scwidget->ideal_h;
    128 	else
    129 		self->ideal_w = scwidget->ideal_w;
    130 	ltk_recalculate_box(self);
    131 
    132 	return id;
    133 }
    134 
    135 static void
    136 ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
    137 	ltk_box *box = LTK_CAST_BOX(self);
    138 	int delta = 0;
    139 	if (box->orient == LTK_HORIZONTAL) {
    140 		if (r.x + r.w > self->lrect.w && r.w <= self->lrect.w)
    141 			delta = r.x - (self->lrect.w - r.w);
    142 		else if (r.x < 0 || r.w > self->lrect.w)
    143 			delta = r.x;
    144 	} else {
    145 		if (r.y + r.h > self->lrect.h && r.h <= self->lrect.h)
    146 			delta = r.y - (self->lrect.h - r.h);
    147 		else if (r.y < 0 || r.h > self->lrect.h)
    148 			delta = r.y;
    149 	}
    150 	if (delta)
    151 		ltk_scrollbar_scroll(box->scrollbar, delta, 0);
    152 }
    153 
    154 static void
    155 ltk_box_destroy(ltk_widget *self, int shallow) {
    156 	ltk_box *box = LTK_CAST_BOX(self);
    157 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    158 		ltk_widget *ptr = ltk_get_widget_from_id(ltk_array_get(box->widgets, i));
    159 		ptr->parent = LTK_WIDGET_ID_NONE;
    160 		if (!shallow)
    161 			ltk_widget_destroy(ptr, shallow);
    162 	}
    163 	ltk_array_destroy(widget_id, box->widgets);
    164 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    165 	scwidget->parent = LTK_WIDGET_ID_NONE;
    166 	ltk_widget_destroy(scwidget, 0);
    167 	ltk_free(box);
    168 }
    169 
    170 /* FIXME: Make this function name more consistent */
    171 /* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
    172    virtual_size is set - this can cause problems when a widget changes its size
    173    (in the scrolled direction) when resized. */
    174 /* FIXME: avoid complete recalculation when just scrolling (only position updated) */
    175 static void
    176 ltk_recalculate_box(ltk_widget *self) {
    177 	ltk_box *box = LTK_CAST_BOX(self);
    178 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    179 	ltk_scrollbar *sc = LTK_CAST_SCROLLBAR(scwidget);
    180 	ltk_rect *sc_rect = &scwidget->lrect;
    181 	int cur_pos = 0;
    182 	if (box->orient == LTK_HORIZONTAL)
    183 		sc_rect->h = scwidget->ideal_h;
    184 	else
    185 		sc_rect->w = scwidget->ideal_w;
    186 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    187 		ltk_widget *ptr = ltk_get_widget_from_id(ltk_array_get(box->widgets, i));
    188 		ptr->lrect.w = ptr->ideal_w;
    189 		ptr->lrect.h = ptr->ideal_h;
    190 		if (box->orient == LTK_HORIZONTAL) {
    191 			ptr->lrect.x = cur_pos - sc->cur_pos;
    192 			if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
    193 				ptr->lrect.h = self->lrect.h - sc_rect->h;
    194 			if (ptr->sticky & LTK_STICKY_TOP)
    195 				ptr->lrect.y = 0;
    196 			else if (ptr->sticky & LTK_STICKY_BOTTOM)
    197 				ptr->lrect.y = self->lrect.h - ptr->lrect.h - sc_rect->h;
    198 			else
    199 				ptr->lrect.y = (self->lrect.h - ptr->lrect.h) / 2;
    200 			cur_pos += ptr->lrect.w;
    201 		} else {
    202 			ptr->lrect.y = cur_pos - sc->cur_pos;
    203 			if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
    204 				ptr->lrect.w = self->lrect.w - sc_rect->w;
    205 			if (ptr->sticky & LTK_STICKY_LEFT)
    206 				ptr->lrect.x = 0;
    207 			else if (ptr->sticky & LTK_STICKY_RIGHT)
    208 				ptr->lrect.x = self->lrect.w - ptr->lrect.w - sc_rect->w;
    209 			else
    210 				ptr->lrect.x = (self->lrect.w - ptr->lrect.w) / 2;
    211 			cur_pos += ptr->lrect.h;
    212 		}
    213 		ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
    214 		ltk_widget_resize(ptr);
    215 	}
    216 	ltk_scrollbar_set_virtual_size(box->scrollbar, cur_pos);
    217 	if (box->orient == LTK_HORIZONTAL) {
    218 		sc_rect->x = 0;
    219 		sc_rect->y = self->lrect.h - sc_rect->h;
    220 		sc_rect->w = self->lrect.w;
    221 	} else {
    222 		sc_rect->x = self->lrect.w - sc_rect->w;
    223 		sc_rect->y = 0;
    224 		sc_rect->h = self->lrect.h;
    225 	}
    226 	*sc_rect = ltk_rect_intersect(*sc_rect, (ltk_rect){0, 0, self->lrect.w, self->lrect.h});
    227 	scwidget->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, *sc_rect);
    228 	ltk_widget_resize(scwidget);
    229 }
    230 
    231 static void
    232 ltk_box_recalc_ideal_size(ltk_widget *self) {
    233 	ltk_box *box = LTK_CAST_BOX(self);
    234 	self->ideal_w = self->ideal_h = 0;
    235 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    236 		ltk_widget *ptr = ltk_get_widget_from_id(ltk_array_get(box->widgets, i));
    237 		ltk_widget_recalc_ideal_size(ptr);
    238 		if (box->orient == LTK_HORIZONTAL && ptr->ideal_h > self->ideal_h) {
    239 			self->ideal_h = ptr->ideal_h;
    240 			self->ideal_w += ptr->ideal_w;
    241 		} else if (box->orient == LTK_VERTICAL && ptr->ideal_w > self->ideal_w) {
    242 			self->ideal_w = ptr->ideal_w;
    243 			self->ideal_h += ptr->ideal_h;
    244 		}
    245 	}
    246 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    247 	ltk_widget_recalc_ideal_size(scwidget);
    248 	if (box->orient == LTK_HORIZONTAL)
    249 		self->ideal_h += scwidget->ideal_h;
    250 	else if (box->orient == LTK_VERTICAL)
    251 		self->ideal_w += scwidget->ideal_w;
    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 /* In the case with the label, there would need to be a way to say what the ideal
    261    height is, given the current width, but I guess that eventually brings us to
    262    a constraint solver, which I'd like to avoid... */
    263 
    264 static void
    265 ltk_box_child_size_change(ltk_widget *self, ltk_widget_id childid) {
    266 	ltk_box *box = LTK_CAST_BOX(self);
    267 	ltk_widget *child = ltk_get_widget_from_id(childid);
    268 	short size_changed = 0;
    269 	/* This is always reset here - if it needs to be changed,
    270 	   the resize function called by the last child_size_change
    271 	   function will fix it */
    272 	/* Note: This seems a bit weird, but if each widget set its rect itself,
    273 	   that would also lead to weird things. For instance, if a butten is
    274 	   added to a box after being ungridded, and its rect was changed
    275 	   by the grid (e.g. because of a column weight), who should reset the
    276 	   rect if it doesn't have sticky set? Of course, the resize function
    277 	   could also set all widgets even if they don't have any sticky
    278 	   settings, but there'd probably be some catch as well. */
    279 	/* FIXME: the same comment as in grid.c applies */
    280 	int orig_w = child->lrect.w;
    281 	int orig_h = child->lrect.h;
    282 	child->lrect.w = child->ideal_w;
    283 	child->lrect.h = child->ideal_h;
    284 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    285 	int sc_w = scwidget->lrect.w;
    286 	int sc_h = scwidget->lrect.h;
    287 	if (box->orient == LTK_HORIZONTAL && child->ideal_h + sc_h > self->ideal_h) {
    288 		self->ideal_h = child->ideal_h + sc_h;
    289 		size_changed = 1;
    290 	} else if (box->orient == LTK_VERTICAL && child->ideal_w + sc_w > self->ideal_h) {
    291 		self->ideal_w = child->ideal_w + sc_w;
    292 		size_changed = 1;
    293 	}
    294 
    295 	ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
    296 	if (size_changed && parent && parent->vtable->child_size_change)
    297 		parent->vtable->child_size_change(parent, self->id);
    298 	else
    299 		ltk_recalculate_box(self);
    300 	if (orig_w != child->lrect.w || orig_h != child->lrect.h)
    301 		ltk_widget_resize(child);
    302 }
    303 
    304 int
    305 ltk_box_add(ltk_widget_id boxid, ltk_widget_id widgetid, ltk_sticky_mask sticky) {
    306 	ltk_widget *self = ltk_get_widget_from_id(boxid);
    307 	ltk_box *box = LTK_CAST_BOX(self);
    308 	ltk_widget *widget = ltk_get_widget_from_id(widgetid);
    309 	if (!LTK_WIDGET_ID_IS_NONE(widget->parent))
    310 		return 1;
    311 	ltk_widget_recalc_ideal_size(widget);
    312 
    313 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    314 	int sc_w = scwidget->lrect.w;
    315 	int sc_h = scwidget->lrect.h;
    316 
    317 	ltk_array_append(widget_id, box->widgets, widgetid);
    318 	if (box->orient == LTK_HORIZONTAL) {
    319 		self->ideal_w += widget->ideal_w;
    320 		if (widget->ideal_h + sc_h > self->ideal_h)
    321 			self->ideal_h = widget->ideal_h + sc_h;
    322 	} else {
    323 		self->ideal_h += widget->ideal_h;
    324 		if (widget->ideal_w + sc_w > self->ideal_w)
    325 			self->ideal_w = widget->ideal_w + sc_w;
    326 	}
    327 	widget->parent = self->id;
    328 	widget->sticky = sticky;
    329 	ltk_box_child_size_change(self, widgetid);
    330 	ltk_window_invalidate_widget_rect(self->window, self->id);
    331 
    332 	return 0;
    333 }
    334 
    335 int
    336 ltk_box_remove_index(ltk_widget_id boxid, size_t index) {
    337 	ltk_widget *self = ltk_get_widget_from_id(boxid);
    338 	ltk_box *box = LTK_CAST_BOX(self);
    339 	if (index >= ltk_array_len(box->widgets))
    340 		return 1;
    341 	ltk_widget *widget = ltk_get_widget_from_id(ltk_array_get(box->widgets, index));
    342 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    343 	int sc_w = scwidget->lrect.w;
    344 	int sc_h = scwidget->lrect.h;
    345 	ltk_array_delete(widget_id, box->widgets, index, 1);
    346 	ltk_window_invalidate_widget_rect(self->window, self->id);
    347 	/* search for new ideal width/height */
    348 	/* FIXME: make this all a bit nicer and break the lines better */
    349 	/* FIXME: other part of ideal size not updated */
    350 	if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == self->ideal_h) {
    351 		self->ideal_h = 0;
    352 		for (size_t j = 0; j < ltk_array_len(box->widgets); j++) {
    353 			ltk_widget *w = ltk_get_widget_from_id(ltk_array_get(box->widgets, j));
    354 			if (w->ideal_h + sc_h > self->ideal_h)
    355 				self->ideal_h = w->ideal_h + sc_h;
    356 		}
    357 		ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
    358 		if (parent)
    359 			ltk_widget_resize(parent);
    360 	} else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == self->ideal_w) {
    361 		self->ideal_w = 0;
    362 		for (size_t j = 0; j < ltk_array_len(box->widgets); j++) {
    363 			ltk_widget *w = ltk_get_widget_from_id(ltk_array_get(box->widgets, j));
    364 			if (w->ideal_w + sc_w > self->ideal_w)
    365 				self->ideal_w = w->ideal_w + sc_w;
    366 		}
    367 		ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
    368 		if (parent)
    369 			ltk_widget_resize(parent);
    370 	}
    371 	return 0;
    372 }
    373 
    374 int
    375 ltk_box_remove(ltk_widget_id boxid, ltk_widget_id widgetid) {
    376 	ltk_widget *self = ltk_get_widget_from_id(boxid);
    377 	ltk_box *box = LTK_CAST_BOX(self);
    378 	ltk_widget *widget = ltk_get_widget_from_id(widgetid);
    379 	if (!LTK_WIDGET_ID_EQUAL(widget->parent, self->id))
    380 		return 1;
    381 	widget->parent = LTK_WIDGET_ID_NONE;
    382 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    383 		ltk_widget_id id = ltk_array_get(box->widgets, i);
    384 		if (LTK_WIDGET_ID_EQUAL(id, widgetid))
    385 			return ltk_box_remove_index(boxid, i);
    386 	}
    387 
    388 	return 1;
    389 }
    390 
    391 static int
    392 ltk_box_remove_child(ltk_widget *self, ltk_widget_id widgetid) {
    393 	return ltk_box_remove(self->id, widgetid);
    394 }
    395 
    396 /* FIXME: maybe come up with a more efficient method */
    397 static ltk_widget_id
    398 ltk_box_nearest_child(ltk_widget *self, ltk_rect rect) {
    399 	ltk_box *box = LTK_CAST_BOX(self);
    400 	ltk_widget_id minw = LTK_WIDGET_ID_NONE;
    401 	int min_dist = INT_MAX;
    402 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    403 		ltk_widget_id id = ltk_array_get(box->widgets, i);
    404 		ltk_widget *widget = ltk_get_widget_from_id(id);
    405 		ltk_rect r = widget->lrect;
    406 		int dist = ltk_rect_fakedist(rect, r);
    407 		if (dist < min_dist) {
    408 			min_dist = dist;
    409 			minw = id;
    410 		}
    411 	}
    412 	return minw;
    413 }
    414 
    415 static ltk_widget_id
    416 ltk_box_nearest_child_left(ltk_widget *self, ltk_widget_id childid) {
    417 	ltk_box *box = LTK_CAST_BOX(self);
    418 	if (box->orient == LTK_VERTICAL)
    419 		return LTK_WIDGET_ID_NONE;
    420 	return ltk_box_prev_child(self, childid);
    421 }
    422 
    423 static ltk_widget_id
    424 ltk_box_nearest_child_right(ltk_widget *self, ltk_widget_id childid) {
    425 	ltk_box *box = LTK_CAST_BOX(self);
    426 	if (box->orient == LTK_VERTICAL)
    427 		return LTK_WIDGET_ID_NONE;
    428 	return ltk_box_next_child(self, childid);
    429 }
    430 
    431 static ltk_widget_id
    432 ltk_box_nearest_child_above(ltk_widget *self, ltk_widget_id childid) {
    433 	ltk_box *box = LTK_CAST_BOX(self);
    434 	if (box->orient == LTK_HORIZONTAL)
    435 		return LTK_WIDGET_ID_NONE;
    436 	return ltk_box_prev_child(self, childid);
    437 }
    438 
    439 static ltk_widget_id
    440 ltk_box_nearest_child_below(ltk_widget *self, ltk_widget_id childid) {
    441 	ltk_box *box = LTK_CAST_BOX(self);
    442 	if (box->orient == LTK_HORIZONTAL)
    443 		return LTK_WIDGET_ID_NONE;
    444 	return ltk_box_next_child(self, childid);
    445 }
    446 
    447 static ltk_widget_id
    448 ltk_box_prev_child(ltk_widget *self, ltk_widget_id childid) {
    449 	ltk_box *box = LTK_CAST_BOX(self);
    450 	for (size_t i = ltk_array_len(box->widgets); i-- > 0;) {
    451 		ltk_widget_id id = ltk_array_get(box->widgets, i);
    452 		if (LTK_WIDGET_ID_EQUAL(id, childid))
    453 			return i > 0 ? ltk_array_get(box->widgets, i - 1) : LTK_WIDGET_ID_NONE;
    454 	}
    455 	return LTK_WIDGET_ID_NONE;
    456 }
    457 
    458 static ltk_widget_id
    459 ltk_box_next_child(ltk_widget *self, ltk_widget_id childid) {
    460 	ltk_box *box = LTK_CAST_BOX(self);
    461 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    462 		ltk_widget_id id = ltk_array_get(box->widgets, i);
    463 		if (LTK_WIDGET_ID_EQUAL(id, childid))
    464 			return i < ltk_array_len(box->widgets) - 1 ? ltk_array_get(box->widgets, i + 1) : LTK_WIDGET_ID_NONE;
    465 	}
    466 	return LTK_WIDGET_ID_NONE;
    467 }
    468 
    469 static ltk_widget_id
    470 ltk_box_first_child(ltk_widget *self) {
    471 	ltk_box *box = LTK_CAST_BOX(self);
    472 	return ltk_array_len(box->widgets) > 0 ? ltk_array_get(box->widgets, 0) : LTK_WIDGET_ID_NONE;
    473 }
    474 
    475 static ltk_widget_id
    476 ltk_box_last_child(ltk_widget *self) {
    477 	ltk_box *box = LTK_CAST_BOX(self);
    478 	size_t len = ltk_array_len(box->widgets);
    479 	return len > 0 ? ltk_array_get(box->widgets, len - 1) : LTK_WIDGET_ID_NONE;
    480 }
    481 
    482 static int
    483 ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
    484 	(void)self;
    485 	(void)args;
    486 	ltk_widget_id boxid = LTK_CAST_ARG_WIDGET_ID(data);
    487 	ltk_widget *boxw = ltk_get_widget_from_id(boxid);
    488 	ltk_recalculate_box(boxw);
    489 	ltk_window_invalidate_widget_rect(boxw->window, boxw->id);
    490 	return 1;
    491 }
    492 
    493 static ltk_widget_id
    494 ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
    495 	ltk_box *box = LTK_CAST_BOX(self);
    496 	ltk_widget *scwidget = ltk_get_widget_from_id(box->scrollbar);
    497 	if (ltk_collide_rect(scwidget->crect, x, y))
    498 		return box->scrollbar;
    499 	for (size_t i = 0; i < ltk_array_len(box->widgets); i++) {
    500 		ltk_widget *widget = ltk_get_widget_from_id(ltk_array_get(box->widgets, i));
    501 		if (ltk_collide_rect(widget->crect, x, y))
    502 			return widget->id;
    503 	}
    504 	return LTK_WIDGET_ID_NONE;
    505 }
    506 
    507 static int
    508 ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
    509 	ltk_box *box = LTK_CAST_BOX(self);
    510 	if (event->dy) {
    511 		/* FIXME: horizontal scrolling, etc. */
    512 		/* FIXME: configure scrollstep */
    513 		int delta = event->dy * -15;
    514 		ltk_scrollbar_scroll(box->scrollbar, delta, 0);
    515 		ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
    516 		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
    517 		return 1;
    518 	}
    519 	return 0;
    520 }