radiobutton.c - ltk - GUI toolkit for X11 (WIP)
HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
radiobutton.c (13464B)
---
1 /*
2 * Copyright (c) 2024-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 #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 = <k_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 = <k_radiobutton_draw,
55 .destroy = <k_radiobutton_destroy,
56 .child_size_change = NULL,
57 .remove_child = NULL,
58 .recalc_ideal_size = <k_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_widget *prev = ltk_get_widget_or_null_from_id(button->prev);
214 ltk_widget *next = ltk_get_widget_or_null_from_id(button->next);
215 while (prev) {
216 ltk_radiobutton_set_checked(prev->id, 0);
217 ltk_radiobutton *cur_btn = LTK_CAST_RADIOBUTTON(prev);
218 prev = ltk_get_widget_or_null_from_id(cur_btn->prev);
219 }
220 while (next) {
221 ltk_radiobutton_set_checked(next->id, 0);
222 ltk_radiobutton *cur_btn = LTK_CAST_RADIOBUTTON(next);
223 next = ltk_get_widget_or_null_from_id(cur_btn->next);
224 }
225 }
226
227 static int
228 ltk_radiobutton_release(ltk_widget *self) {
229 ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
230 button->checked = !button->checked;
231 if (button->checked)
232 uncheck_other_buttons(button);
233 ltk_widget_emit_signal(self->id, LTK_RADIOBUTTON_SIGNAL_CHANGED, LTK_EMPTY_ARGLIST);
234 return 1;
235 }
236
237 int
238 ltk_radiobutton_get_checked(ltk_widget_id buttonid) {
239 ltk_widget *self = ltk_get_widget_from_id(buttonid);
240 ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
241 return button->checked;
242 }
243
244 void
245 ltk_radiobutton_set_checked(ltk_widget_id buttonid, int checked) {
246 ltk_widget *self = ltk_get_widget_from_id(buttonid);
247 ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
248 button->checked = checked;
249 if (checked)
250 uncheck_other_buttons(button);
251 ltk_window_invalidate_widget_rect(self->window, self->id);
252 }
253
254 #define MAX(a, b) ((a) > (b) ? (a) : (b))
255
256 static void
257 recalc_ideal_size(ltk_radiobutton *button) {
258 ltk_widget *self = LTK_CAST_WIDGET(button);
259 int text_w, text_h;
260 ltk_text_line_get_size(button->tl, &text_w, &text_h);
261 int circle_size = ltk_size_to_pixel(theme.circle_size, self->last_dpi);
262 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
263 self->ideal_w = text_w + pad * 3 + circle_size;
264 self->ideal_h = MAX(text_h, circle_size) + pad * 2;
265 }
266
267 static void
268 ltk_radiobutton_recalc_ideal_size(ltk_widget *self) {
269 ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
270 int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi);
271 ltk_text_line_set_font_size(button->tl, font_size);
272 recalc_ideal_size(button);
273 }
274
275 ltk_widget_id
276 ltk_radiobutton_create(ltk_widget_id windowid, const char *text, int checked, ltk_widget_id group_memberid) {
277 ltk_radiobutton *button = ltk_malloc(sizeof(ltk_radiobutton));
278 ltk_widget_id id = ltk_initialize_widget(LTK_CAST_WIDGET(button), windowid, &vtable, 0, 0);
279 button->checked = checked;
280 button->prev = button->next = LTK_WIDGET_ID_NONE;
281 ltk_widget *group_memberw = ltk_get_widget_or_null_from_id(group_memberid);
282 if (group_memberw) {
283 button->prev = group_memberid;
284 ltk_radiobutton *group_member = LTK_CAST_RADIOBUTTON(group_memberw);
285 button->next = group_member->next;
286 group_member->next = id;
287 /* I guess it's technically possible for a button that is only created and
288 never added to the widget hierarchy to cause the other buttons to be
289 unchecked, but I guess that's just the way it is. */
290 if (button->checked)
291 uncheck_other_buttons(button);
292 }
293
294 button->tl = ltk_text_line_create_const_text_default(
295 theme.font, ltk_size_to_pixel(theme.font_size, button->widget.last_dpi), text, -1
296 );
297 recalc_ideal_size(button);
298 LTK_CAST_WIDGET(button)->dirty = 1;
299
300 return id;
301 }
302
303 static void
304 ltk_radiobutton_destroy(ltk_widget *self, int shallow) {
305 (void)shallow;
306 ltk_radiobutton *button = LTK_CAST_RADIOBUTTON(self);
307 ltk_text_line_destroy(button->tl);
308 ltk_widget *prevw = ltk_get_widget_or_null_from_id(button->prev);
309 if (prevw) {
310 ltk_radiobutton *prev = LTK_CAST_RADIOBUTTON(prevw);
311 prev->next = button->next;
312 }
313 ltk_widget *nextw = ltk_get_widget_or_null_from_id(button->next);
314 if (nextw) {
315 ltk_radiobutton *next = LTK_CAST_RADIOBUTTON(nextw);
316 next->prev = button->prev;
317 }
318 ltk_free(button);
319 }