URI:
       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 = &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_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 }