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 = <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_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 }