ltk

GUI toolkit for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/ltk.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
Log | Files | Refs | README | LICENSE

radiobutton.c (12701B)


      1 /*
      2  * Copyright (c) 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 #include <stdio.h>
     18 
     19 #include "config.h"
     20 #include "radiobutton.h"
     21 #include "color.h"
     22 #include "graphics.h"
     23 #include "ltk.h"
     24 #include "memory.h"
     25 #include "rect.h"
     26 #include "text.h"
     27 #include "util.h"
     28 #include "widget.h"
     29 
     30 /* FIXME: a lot of duplicated code from checkbutton */
     31 
     32 #define MAX_RADIOBUTTON_BORDER_WIDTH 10000
     33 #define MAX_RADIOBUTTON_PADDING 50000
     34 #define MAX_RADIOBUTTON_CIRCLE_SIZE 50000
     35 
     36 static void ltk_radiobutton_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
     37 static int ltk_radiobutton_release(ltk_widget *self);
     38 static void ltk_radiobutton_destroy(ltk_widget *self, int shallow);
     39 static void ltk_radiobutton_recalc_ideal_size(ltk_widget *self);
     40 
     41 static struct ltk_widget_vtable vtable = {
     42 	.key_press = NULL,
     43 	.key_release = NULL,
     44 	.mouse_press = NULL,
     45 	.mouse_release = NULL,
     46 	.release = &ltk_radiobutton_release,
     47 	.motion_notify = NULL,
     48 	.mouse_leave = NULL,
     49 	.mouse_enter = NULL,
     50 	.change_state = NULL,
     51 	.get_child_at_pos = NULL,
     52 	.resize = NULL,
     53 	.hide = NULL,
     54 	.draw = &ltk_radiobutton_draw,
     55 	.destroy = &ltk_radiobutton_destroy,
     56 	.child_size_change = NULL,
     57 	.remove_child = NULL,
     58 	.recalc_ideal_size = &ltk_radiobutton_recalc_ideal_size,
     59 	.type = LTK_WIDGET_RADIOBUTTON,
     60 	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
     61 	.invalid_signal = LTK_RADIOBUTTON_SIGNAL_INVALID,
     62 };
     63 
     64 static struct {
     65 	ltk_color *text_color;
     66 
     67 	ltk_color *fill;
     68 	ltk_color *fill_pressed;
     69 	ltk_color *fill_hover;
     70 	ltk_color *fill_active;
     71 	ltk_color *fill_disabled;
     72 
     73 	ltk_color *circle_fill;
     74 	ltk_color *circle_border;
     75 
     76 	ltk_color *circle_fill_pressed;
     77 	ltk_color *circle_border_pressed;
     78 
     79 	ltk_color *circle_fill_hover;
     80 	ltk_color *circle_border_hover;
     81 
     82 	ltk_color *circle_fill_active;
     83 	ltk_color *circle_border_active;
     84 
     85 	ltk_color *circle_fill_disabled;
     86 	ltk_color *circle_border_disabled;
     87 
     88 	ltk_color *circle_fill_checked;
     89 	ltk_color *circle_border_checked;
     90 
     91 	ltk_color *circle_fill_pressed_checked;
     92 	ltk_color *circle_border_pressed_checked;
     93 
     94 	ltk_color *circle_fill_hover_checked;
     95 	ltk_color *circle_border_hover_checked;
     96 
     97 	ltk_color *circle_fill_active_checked;
     98 	ltk_color *circle_border_active_checked;
     99 
    100 	ltk_color *circle_fill_disabled_checked;
    101 	ltk_color *circle_border_disabled_checked;
    102 
    103 	char *font;
    104 	ltk_size circle_size;
    105 	ltk_size circle_border_width;
    106 	ltk_size pad;
    107 	ltk_size font_size;
    108 } theme;
    109 
    110 static ltk_theme_parseinfo parseinfo[] = {
    111 	{"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#000000"}, 0, 0, 0},
    112 	{"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#222222"}, 0, 0, 0},
    113 	{"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#222222"}, 0, 0, 0},
    114 	{"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
    115 	{"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#222222"}, 0, 0, 0},
    116 
    117 	{"circle-fill", THEME_COLOR, {.color = &theme.circle_fill}, {.color = "#000000"}, 0, 0, 0},
    118 	{"circle-fill-hover", THEME_COLOR, {.color = &theme.circle_fill_hover}, {.color = "#222222"}, 0, 0, 0},
    119 	{"circle-fill-active", THEME_COLOR, {.color = &theme.circle_fill_active}, {.color = "#222222"}, 0, 0, 0},
    120 	{"circle-fill-disabled", THEME_COLOR, {.color = &theme.circle_fill_disabled}, {.color = "#292929"}, 0, 0, 0},
    121 	{"circle-fill-pressed", THEME_COLOR, {.color = &theme.circle_fill_pressed}, {.color = "#222222"}, 0, 0, 0},
    122 	{"circle-border", THEME_COLOR, {.color = &theme.circle_border}, {.color = "#FFFFFF"}, 0, 0, 0},
    123 	{"circle-border-hover", THEME_COLOR, {.color = &theme.circle_border_hover}, {.color = "#FFFFFF"}, 0, 0, 0},
    124 	{"circle-border-active", THEME_COLOR, {.color = &theme.circle_border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
    125 	{"circle-border-disabled", THEME_COLOR, {.color = &theme.circle_border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
    126 	{"circle-border-pressed", THEME_COLOR, {.color = &theme.circle_border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
    127 
    128 	{"circle-fill-checked", THEME_COLOR, {.color = &theme.circle_fill_checked}, {.color = "#113355"}, 0, 0, 0},
    129 	{"circle-fill-hover-checked", THEME_COLOR, {.color = &theme.circle_fill_hover_checked}, {.color = "#738194"}, 0, 0, 0},
    130 	{"circle-fill-active-checked", THEME_COLOR, {.color = &theme.circle_fill_active_checked}, {.color = "#113355"}, 0, 0, 0},
    131 	{"circle-fill-disabled-checked", THEME_COLOR, {.color = &theme.circle_fill_disabled_checked}, {.color = "#292929"}, 0, 0, 0},
    132 	{"circle-fill-pressed-checked", THEME_COLOR, {.color = &theme.circle_fill_pressed_checked}, {.color = "#113355"}, 0, 0, 0},
    133 	{"circle-border-checked", THEME_COLOR, {.color = &theme.circle_border_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
    134 	{"circle-border-hover-checked", THEME_COLOR, {.color = &theme.circle_border_hover_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
    135 	{"circle-border-active-checked", THEME_COLOR, {.color = &theme.circle_border_active_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
    136 	{"circle-border-disabled-checked", THEME_COLOR, {.color = &theme.circle_border_disabled_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
    137 	{"circle-border-pressed-checked", THEME_COLOR, {.color = &theme.circle_border_pressed_checked}, {.color = "#FFFFFF"}, 0, 0, 0},
    138 
    139 	{"circle-size", THEME_SIZE, {.size = &theme.circle_size}, {.size = {.val = 500, .unit = LTK_UNIT_MM}}, 0, MAX_RADIOBUTTON_CIRCLE_SIZE, 0},
    140 	{"circle-border-width", THEME_SIZE, {.size = &theme.circle_border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_RADIOBUTTON_BORDER_WIDTH, 0},
    141 	{"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_RADIOBUTTON_PADDING, 0},
    142 	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
    143 	{"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
    144 	{"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0},
    145 };
    146 
    147 void
    148 ltk_radiobutton_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
    149 	*p = parseinfo;
    150 	*len = LENGTH(parseinfo);
    151 }
    152 
    153 /* FIXME: a lot more theme settings */
    154 static void
    155 ltk_radiobutton_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
    156 	ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
    157 	ltk_rect lrect = self->lrect;
    158 	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
    159 	if (clip_final.w <= 0 || clip_final.h <= 0)
    160 		return;
    161 
    162 	int circle_size = ltk_size_to_pixel(theme.circle_size, self->last_dpi);
    163 	int circle_bw = ltk_size_to_pixel(theme.circle_border_width, self->last_dpi);
    164 	int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
    165 	ltk_color *fill = NULL, *circle_border = NULL, *circle_fill = NULL;
    166 	if (self->state & LTK_DISABLED) {
    167 		fill = theme.fill_disabled;
    168 		circle_border = button->checked ? theme.circle_border_disabled_checked : theme.circle_border_disabled;
    169 		circle_fill = button->checked ? theme.circle_fill_disabled_checked : theme.circle_fill_disabled;
    170 	} else if (self->state & LTK_PRESSED) {
    171 		fill = theme.fill_pressed;
    172 		circle_border = button->checked ? theme.circle_border_pressed_checked : theme.circle_border_pressed;
    173 		circle_fill = button->checked ? theme.circle_fill_pressed_checked : theme.circle_fill_pressed;
    174 	} else if (self->state & LTK_HOVER) {
    175 		fill = theme.fill_hover;
    176 		circle_border = button->checked ? theme.circle_border_hover_checked : theme.circle_border_hover;
    177 		circle_fill = button->checked ? theme.circle_fill_hover_checked : theme.circle_fill_hover;
    178 	} else if (self->state & LTK_ACTIVE) {
    179 		fill = theme.fill_active;
    180 		circle_border = button->checked ? theme.circle_border_active_checked : theme.circle_border_active;
    181 		circle_fill = button->checked ? theme.circle_fill_active_checked : theme.circle_fill_active;
    182 	} else {
    183 		fill = theme.fill;
    184 		circle_border = button->checked ? theme.circle_border_checked : theme.circle_border;
    185 		circle_fill = button->checked ? theme.circle_fill_checked : theme.circle_fill;
    186 	}
    187 	ltk_rect circle_rect = {x + pad, y + pad, circle_size, circle_size};
    188 	ltk_rect draw_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
    189 	ltk_surface_fill_rect(draw_surf, fill, draw_clip);
    190 	/* Yeah, this draws the inner part of the circle twice... */
    191 	if (circle_bw > 0) {
    192 		ltk_surface_fill_ellipse_clipped(draw_surf, circle_border, circle_rect, draw_clip);
    193 	}
    194 	if (circle_size > circle_bw * 2) {
    195 		circle_rect.x += circle_bw;
    196 		circle_rect.y += circle_bw;
    197 		circle_rect.w -= circle_bw * 2;
    198 		circle_rect.h -= circle_bw * 2;
    199 		ltk_surface_fill_ellipse_clipped(draw_surf, circle_fill, circle_rect, draw_clip);
    200 	}
    201 	int text_w, text_h;
    202 	ltk_text_line_get_size(button->tl, &text_w, &text_h);
    203 	int text_x = x + 2 * pad + circle_size;
    204 	int text_y = y + (lrect.h - text_h) / 2;
    205 	ltk_text_line_draw_clipped(button->tl, draw_surf, theme.text_color, text_x, text_y, draw_clip);
    206 	/* FIXME: only redraw if dirty (needs to be handled higher-up to only
    207 	   call draw when dirty or window rect invalidated */
    208 	self->dirty = 0;
    209 }
    210 
    211 static void
    212 uncheck_other_buttons(ltk_radiobutton *button) {
    213 	ltk_radiobutton *prev = button->prev, *next = button->next;
    214 	while (prev) {
    215 		ltk_radiobutton_set_checked(prev, 0);
    216 		prev = prev->prev;
    217 	}
    218 	while (next) {
    219 		ltk_radiobutton_set_checked(next, 0);
    220 		next = next->next;
    221 	}
    222 }
    223 
    224 static int
    225 ltk_radiobutton_release(ltk_widget *self) {
    226 	ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
    227 	button->checked = !button->checked;
    228 	if (button->checked)
    229 		uncheck_other_buttons(button);
    230 	ltk_widget_emit_signal(self, LTK_RADIOBUTTON_SIGNAL_CHANGED, LTK_EMPTY_ARGLIST);
    231 	return 1;
    232 }
    233 
    234 int
    235 ltk_radiobutton_get_checked(ltk_radiobutton *button) {
    236 	return button->checked;
    237 }
    238 
    239 void
    240 ltk_radiobutton_set_checked(ltk_radiobutton *button, int checked) {
    241 	button->checked = checked;
    242 	if (checked)
    243 		uncheck_other_buttons(button);
    244 	ltk_widget *self = LTK_CAST_WIDGET(button);
    245 	ltk_window_invalidate_widget_rect(self->window, self);
    246 }
    247 
    248 #define MAX(a, b) ((a) > (b) ? (a) : (b))
    249 
    250 static void
    251 recalc_ideal_size(ltk_radiobutton *button) {
    252 	int text_w, text_h;
    253 	ltk_text_line_get_size(button->tl, &text_w, &text_h);
    254 	int circle_size = ltk_size_to_pixel(theme.circle_size, LTK_CAST_WIDGET(button)->last_dpi);
    255 	int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(button)->last_dpi);
    256 	button->widget.ideal_w = text_w + pad * 3 + circle_size;
    257 	button->widget.ideal_h = MAX(text_h, circle_size) + pad * 2;
    258 }
    259 
    260 static void
    261 ltk_radiobutton_recalc_ideal_size(ltk_widget *self) {
    262 	ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
    263 	int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi);
    264 	ltk_text_line_set_font_size(button->tl, font_size);
    265 	recalc_ideal_size(button);
    266 }
    267 
    268 ltk_radiobutton *
    269 ltk_radiobutton_create(ltk_window *window, const char *text, int checked, ltk_radiobutton *group_member) {
    270 	ltk_radiobutton *button = ltk_malloc(sizeof(ltk_radiobutton));
    271 	ltk_fill_widget_defaults(LTK_CAST_WIDGET(button), window, &vtable, 0, 0);
    272 	button->checked = checked;
    273 	button->prev = button->next = NULL;
    274 	if (group_member) {
    275 		button->prev = group_member;
    276 		button->next = group_member->next;
    277 		group_member->next = button;
    278 		/* I guess it's technically possible for a button that is only created and
    279 		   never added to the widget hierarchy to cause the other buttons to be
    280 		   unchecked, but I guess that's just the way it is. */
    281 		if (button->checked)
    282 			uncheck_other_buttons(button);
    283 	}
    284 
    285 	button->tl = ltk_text_line_create_const_text_default(
    286 		theme.font, ltk_size_to_pixel(theme.font_size, button->widget.last_dpi), text, -1
    287 	);
    288 	recalc_ideal_size(button);
    289 	button->widget.dirty = 1;
    290 
    291 	return button;
    292 }
    293 
    294 static void
    295 ltk_radiobutton_destroy(ltk_widget *self, int shallow) {
    296 	(void)shallow;
    297 	ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
    298 	if (!button) {
    299 		ltk_warn("Tried to destroy NULL radiobutton.\n");
    300 		return;
    301 	}
    302 	ltk_text_line_destroy(button->tl);
    303 	if (button->prev)
    304 		button->prev->next = button->next;
    305 	if (button->next)
    306 		button->next->prev = button->prev;
    307 	ltk_free(button);
    308 }