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

commit 71f3528512ae0666e68994bd70dccb933bed55f8
parent 77c8746b41398dffda734c31e1697d2aaba752d4
Author: lumidify <nobody@lumidify.org>
Date:   Sat, 27 Apr 2024 15:10:12 +0200

Add mixed dpi support using xrandr

This is kind of hacky in places still. It probably would be better
to just use 'double' for all size and dpi values instead of having
scaled integers. The config parser also really needs to be cleaned
up now that there are more config options.

Diffstat:
MMakefile | 15+++++++++++----
Mconfig.example/ltk.cfg | 3+++
Mconfig.example/theme.ini | 10+++++-----
Msrc/ltk/box.c | 31++++++++++++++++++++++++++++++-
Msrc/ltk/button.c | 46++++++++++++++++++++++++++++++++--------------
Msrc/ltk/config.c | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/ltk/config.h | 3+++
Msrc/ltk/entry.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/ltk/event.h | 8++++++++
Msrc/ltk/event_xlib.c | 159++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/ltk/eventdefs.h | 1+
Msrc/ltk/graphics.h | 44+++++++++++++++++++++++++++++++++++++++++++-
Msrc/ltk/graphics_xlib.c | 46+++++++++++++++++++++++++++++++++++++++++++++-
Msrc/ltk/graphics_xlib.h | 25++++++++++++++++++++++---
Msrc/ltk/grid.c | 38++++++++++++++++++++++++++++++++++++++
Msrc/ltk/label.c | 37++++++++++++++++++++++++++-----------
Msrc/ltk/ltk.c | 1+
Msrc/ltk/menu.c | 312+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Asrc/ltk/num.c | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/ltk/scrollbar.c | 24++++++++++++++++--------
Dsrc/ltk/strtonum.c | 67-------------------------------------------------------------------
Msrc/ltk/text.h | 3++-
Msrc/ltk/text_pango.c | 13+++++++++----
Msrc/ltk/theme.c | 29++++++++++++++++++++++++++++-
Msrc/ltk/theme.h | 11+++++++++--
Msrc/ltk/util.h | 4++++
Msrc/ltk/widget.c | 20++++++++++++++++++--
Msrc/ltk/widget.h | 14++++++++++++++
Msrc/ltk/window.c | 22++++++++++++++++------
Msrc/ltk/window.h | 6+++++-
30 files changed, 1119 insertions(+), 296 deletions(-)

diff --git a/Makefile b/Makefile @@ -14,6 +14,7 @@ DEV = 1 MEMDEBUG = 0 SANITIZE = 0 USE_PANGO = 1 +USE_XRANDR = 1 # Note: this macro magic for debugging and pango rendering seems ugly; it should probably be changed @@ -33,15 +34,21 @@ EXTRA_OBJ_1 = src/ltk/text_pango.o EXTRA_CFLAGS_1 = `pkg-config --cflags pangoxft` EXTRA_LDFLAGS_1 = `pkg-config --libs pangoxft` +# xrandr support (dynamic dpi) +EXTRA_CFLAGS_XRANDR_0 = +EXTRA_CFLAGS_XRANDR_1 = `pkg-config --cflags xrandr` +EXTRA_LDFLAGS_XRANDR_0 = +EXTRA_LDFLAGS_XRANDR_1 = `pkg-config --libs xrandr` + EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO)) -EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO)) -EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO)) +EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO)) $(EXTRA_CFLAGS_XRANDR_$(USE_XRANDR)) +EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO)) $(EXTRA_LDFLAGS_XRANDR_$(USE_XRANDR)) -LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -I ./src -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor imlib2` -D_POSIX_C_SOURCE=200809L +LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_XRANDR=$(USE_XRANDR) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -I ./src -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor imlib2` -D_POSIX_C_SOURCE=200809L LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor imlib2` OBJ_LTK = \ - src/ltk/strtonum.o \ + src/ltk/num.o \ src/ltk/util.o \ src/ltk/memory.o \ src/ltk/window.o \ diff --git a/config.example/ltk.cfg b/config.example/ltk.cfg @@ -2,6 +2,9 @@ explicit-focus = true all-activatable = true line-editor = "st -e vi %f" +mixed-dpi = true +fixed-dpi = 96 +dpi-scale = 1.0 # In future: # text-editor = ... diff --git a/config.example/theme.ini b/config.example/theme.ini @@ -1,13 +1,13 @@ [window] -font-size = 15 +font-size = 12pt bg = #000000 fg = #FFFFFF font = Liberation Mono [button] -border-width = 2 +border-width = 0.5mm text-color = #FFFFFF -pad = 5 +pad = 1mm border = #339999 fill = #113355 border-pressed = #FFFFFF @@ -19,10 +19,10 @@ fill-disabled = #292929 [label] text-color = #FFFFFF -pad = 5 +pad = 1mm [scrollbar] -size = 15 +size = 3.5mm bg = #000000 bg-disabled = #555555 fg = #113355 diff --git a/src/ltk/box.c b/src/ltk/box.c @@ -49,6 +49,8 @@ static ltk_widget *ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *wid static ltk_widget *ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget); static ltk_widget *ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget); +static void ltk_box_recalc_ideal_size(ltk_widget *self); + static struct ltk_widget_vtable vtable = { .change_state = NULL, .hide = NULL, @@ -76,6 +78,7 @@ static struct ltk_widget_vtable vtable = { .nearest_child_above = &ltk_box_nearest_child_above, .nearest_child_below = &ltk_box_nearest_child_below, .ensure_rect_shown = &ltk_box_ensure_rect_shown, + .recalc_ideal_size = &ltk_box_recalc_ideal_size, .type = LTK_WIDGET_BOX, .flags = 0, .invalid_signal = LTK_BOX_SIGNAL_INVALID, @@ -179,6 +182,8 @@ ltk_recalculate_box(ltk_widget *self) { sc_rect->w = box->sc->widget.ideal_w; for (size_t i = 0; i < box->num_widgets; i++) { ptr = box->widgets[i]; + ptr->lrect.w = ptr->ideal_w; + ptr->lrect.h = ptr->ideal_h; if (box->orient == LTK_HORIZONTAL) { ptr->lrect.x = cur_pos - box->sc->cur_pos; if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM) @@ -220,6 +225,29 @@ ltk_recalculate_box(ltk_widget *self) { ltk_widget_resize(LTK_CAST_WIDGET(box->sc)); } +static void +ltk_box_recalc_ideal_size(ltk_widget *self) { + ltk_box *box = LTK_CAST_BOX(self); + ltk_widget *ptr; + self->ideal_w = self->ideal_h = 0; + for (size_t i = 0; i < box->num_widgets; i++) { + ptr = box->widgets[i]; + ltk_widget_recalc_ideal_size(ptr); + if (box->orient == LTK_HORIZONTAL && ptr->ideal_h > self->ideal_h) { + self->ideal_h = ptr->ideal_h; + self->ideal_w += ptr->ideal_w; + } else if (box->orient == LTK_VERTICAL && ptr->ideal_w > self->ideal_w) { + self->ideal_w = ptr->ideal_w; + self->ideal_h += ptr->ideal_h; + } + } + ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(box->sc)); + if (box->orient == LTK_HORIZONTAL) + self->ideal_h += box->sc->widget.ideal_h; + else if (box->orient == LTK_VERTICAL) + self->ideal_w += box->sc->widget.ideal_w; +} + /* FIXME: This entire resizing thing is a bit weird. For instance, if a label in a vertical box increases its height because its width has been decreased and it is forced to wrap, should that just change the rect or also the @@ -236,7 +264,7 @@ ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) { function will fix it */ /* Note: This seems a bit weird, but if each widget set its rect itself, that would also lead to weird things. For instance, if a butten is - added to after a box after being ungridded, and its rect was changed + added to a box after being ungridded, and its rect was changed by the grid (e.g. because of a column weight), who should reset the rect if it doesn't have sticky set? Of course, the resize function could also set all widgets even if they don't have any sticky @@ -274,6 +302,7 @@ ltk_box_add(ltk_box *box, ltk_widget *widget, ltk_sticky_mask sticky) { box->num_alloc = new_size; box->widgets = new; } + ltk_widget_recalc_ideal_size(widget); int sc_w = box->sc->widget.lrect.w; int sc_h = box->sc->widget.lrect.h; diff --git a/src/ltk/button.c b/src/ltk/button.c @@ -28,13 +28,14 @@ #include "util.h" #include "widget.h" -#define MAX_BUTTON_BORDER_WIDTH 100 -#define MAX_BUTTON_PADDING 500 +#define MAX_BUTTON_BORDER_WIDTH 10000 +#define MAX_BUTTON_PADDING 50000 static void ltk_button_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); static int ltk_button_release(ltk_widget *self); ltk_button *ltk_button_create(ltk_window *window, char *text); static void ltk_button_destroy(ltk_widget *self, int shallow); +static void ltk_button_recalc_ideal_size(ltk_widget *self); static struct ltk_widget_vtable vtable = { .key_press = NULL, @@ -53,15 +54,14 @@ static struct ltk_widget_vtable vtable = { .destroy = &ltk_button_destroy, .child_size_change = NULL, .remove_child = NULL, + .recalc_ideal_size = &ltk_button_recalc_ideal_size, .type = LTK_WIDGET_BUTTON, .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS, .invalid_signal = LTK_BUTTON_SIGNAL_INVALID, }; static struct { - int border_width; ltk_color *text_color; - int pad; ltk_color *border; ltk_color *fill; @@ -77,6 +77,9 @@ static struct { ltk_color *border_disabled; ltk_color *fill_disabled; + + ltk_size border_width; + ltk_size pad; } theme; static ltk_theme_parseinfo parseinfo[] = { @@ -85,13 +88,13 @@ static ltk_theme_parseinfo parseinfo[] = { {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0}, {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0}, {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0}, - {"border-width", THEME_INT, {.i = &theme.border_width}, {.i = 2}, 0, MAX_BUTTON_BORDER_WIDTH, 0}, + {"border-width", THEME_SIZE, {.size = &theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_BUTTON_BORDER_WIDTH, 0}, {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0}, {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#738194"}, 0, 0, 0}, {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0}, {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0}, {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0}, - {"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_BUTTON_PADDING, 0}, + {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_BUTTON_PADDING, 0}, {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0}, }; static int parseinfo_sorted = 0; @@ -119,7 +122,7 @@ ltk_button_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect if (clip_final.w <= 0 || clip_final.h <= 0) return; - int bw = theme.border_width; + int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi); ltk_color *border = NULL, *fill = NULL; /* FIXME: HOVERACTIVE STATE */ if (self->state & LTK_DISABLED) { @@ -164,17 +167,32 @@ ltk_button_release(ltk_widget *self) { return 1; } +static void +recalc_ideal_size(ltk_button *button) { + int text_w, text_h; + ltk_text_line_get_size(button->tl, &text_w, &text_h); + int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(button)->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(button)->last_dpi); + button->widget.ideal_w = text_w + bw * 2 + pad * 2; + button->widget.ideal_h = text_h + bw * 2 + pad * 2; +} + +static void +ltk_button_recalc_ideal_size(ltk_widget *self) { + ltk_button *button = LTK_CAST_BUTTON(self); + int font_size = ltk_size_to_pixel(self->window->theme->font_size, self->last_dpi); + ltk_text_line_set_font_size(button->tl, font_size); + recalc_ideal_size(button); +} + ltk_button * ltk_button_create(ltk_window *window, char *text) { ltk_button *button = ltk_malloc(sizeof(ltk_button)); + ltk_fill_widget_defaults(LTK_CAST_WIDGET(button), window, &vtable, 0, 0); - uint16_t font_size = window->theme->font_size; - button->tl = ltk_text_line_create_default(font_size, text, 0, -1); - int text_w, text_h; - ltk_text_line_get_size(button->tl, &text_w, &text_h); - ltk_fill_widget_defaults(LTK_CAST_WIDGET(button), window, &vtable, button->widget.ideal_w, button->widget.ideal_h); - button->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2; - button->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2; + ltk_size font_size = window->theme->font_size; + button->tl = ltk_text_line_create_default(ltk_size_to_pixel(font_size, button->widget.last_dpi), text, 0, -1); + recalc_ideal_size(button); button->widget.dirty = 1; return button; diff --git a/src/ltk/config.c b/src/ltk/config.c @@ -511,6 +511,13 @@ load_from_text( config->general.explicit_focus = 0; config->general.all_activatable = 0; config->general.line_editor = NULL; + config->general.dpi_scale = 1.0; + config->general.fixed_dpi = 480; /* 5 * 96 */ +#if USE_XRANDR + config->general.mixed_dpi = 1; +#else + config->general.mixed_dpi = 0; +#endif struct lexstate s = {filename, file_contents, len, 0, 1, 0}; struct token tok = next_token(&s); @@ -565,8 +572,45 @@ load_from_text( msg = "Invalid boolean setting"; goto error; } + /* FIXME: warning if set to true but xrandr not enabled */ + } else if (str_array_equal("mixed-dpi", prev2tok.text, prev2tok.len)) { + if (str_array_equal("true", tok.text, tok.len)) { + config->general.mixed_dpi = 1; + } else if (str_array_equal("false", tok.text, tok.len)) { + config->general.mixed_dpi = 0; + } else { + msg = "Invalid boolean setting"; + goto error; + } } else if (str_array_equal("line-editor", prev2tok.text, prev2tok.len)) { config->general.line_editor = ltk_strndup(tok.text, tok.len); + } else if (str_array_equal("fixed-dpi", prev2tok.text, prev2tok.len)) { + /* FIXME: remove this allocation! */ + char *tmp = ltk_strndup(tok.text, tok.len); + /* FIXME: proper min/max values for dpi */ + const char *tmp_err = NULL; + config->general.fixed_dpi = ltk_strtonum(tmp, 10, 4000, &tmp_err); + ltk_free(tmp); + if (tmp_err) { + msg = "Invalid DPI setting"; + goto error; + } + /* because of weird scaling that is currently used, see event_xlib.c */ + config->general.fixed_dpi *= 5; + } else if (str_array_equal("dpi-scale", prev2tok.text, prev2tok.len)) { + /* FIXME: remove this allocation! */ + char *tmp = ltk_strndup(tok.text, tok.len); + const char *tmp_err = NULL; + char *ep = NULL; + /* FIXME: proper min/max values for scale */ + config->general.dpi_scale = ltk_strtoscalednum(tmp, 10, 10000, &ep, &tmp_err); + char c = *ep; + ltk_free(tmp); + if (tmp_err || c != '\0') { + msg = "Invalid DPI scale setting"; + goto error; + } + config->general.dpi_scale /= 100; } else { msg = "Invalid setting"; goto error; diff --git a/src/ltk/config.h b/src/ltk/config.h @@ -53,6 +53,9 @@ typedef struct { typedef struct { char *line_editor; + double dpi_scale; + unsigned int fixed_dpi; + char mixed_dpi; char explicit_focus; char all_activatable; } ltk_general_config; diff --git a/src/ltk/entry.c b/src/ltk/entry.c @@ -41,11 +41,13 @@ #include "util.h" #include "widget.h" -#define MAX_ENTRY_BORDER_WIDTH 100 -#define MAX_ENTRY_PADDING 500 +#define MAX_ENTRY_BORDER_WIDTH 10000 +#define MAX_ENTRY_PADDING 50000 static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); static void ltk_entry_destroy(ltk_widget *self, int shallow); +static void recalc_ideal_size(ltk_entry *entry); +static void ltk_entry_recalc_ideal_size(ltk_widget *self); static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event); static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event); @@ -178,16 +180,15 @@ static struct ltk_widget_vtable vtable = { .destroy = &ltk_entry_destroy, .child_size_change = NULL, .remove_child = NULL, + .recalc_ideal_size = &ltk_entry_recalc_ideal_size, .type = LTK_WIDGET_ENTRY, .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_NEEDS_KEYBOARD, .invalid_signal = LTK_ENTRY_SIGNAL_INVALID, }; static struct { - int border_width; ltk_color *text_color; ltk_color *selection_color; - int pad; ltk_color *border; ltk_color *fill; @@ -203,6 +204,9 @@ static struct { ltk_color *border_disabled; ltk_color *fill_disabled; + + ltk_size border_width; + ltk_size pad; } theme; /* FIXME: @@ -215,13 +219,13 @@ static ltk_theme_parseinfo parseinfo[] = { {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0}, {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#339999"}, 0, 0, 0}, {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0}, - {"border-width", THEME_INT, {.i = &theme.border_width}, {.i = 2}, 0, MAX_ENTRY_BORDER_WIDTH, 0}, {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0}, {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#113355"}, 0, 0, 0}, {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0}, {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0}, {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0}, - {"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_ENTRY_PADDING, 0}, + {"border-width", THEME_SIZE, {.size = &theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_ENTRY_BORDER_WIDTH, 0}, + {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_ENTRY_PADDING, 0}, {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0}, {"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0}, }; @@ -251,7 +255,8 @@ ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect if (clip_final.w <= 0 || clip_final.h <= 0) return; - int bw = theme.border_width; + int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, self->last_dpi); ltk_color *border = NULL, *fill = NULL; /* FIXME: HOVERACTIVE STATE */ if (self->state & LTK_DISABLED) { @@ -283,11 +288,11 @@ ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect ltk_text_line_get_size(entry->tl, &text_w, &text_h); /* FIXME: what if text_h > rect.h? */ int x_offset = 0; - if (text_w < lrect.w - 2 * (bw + theme.pad) && ltk_text_line_get_softline_direction(entry->tl, 0) == LTK_TEXT_RTL) - x_offset = lrect.w - 2 * (bw + theme.pad) - text_w; - int text_x = x + bw + theme.pad + x_offset; + if (text_w < lrect.w - 2 * (bw + pad) && ltk_text_line_get_softline_direction(entry->tl, 0) == LTK_TEXT_RTL) + x_offset = lrect.w - 2 * (bw + pad) - text_w; + int text_x = x + bw + pad + x_offset; int text_y = y + (lrect.h - text_h) / 2; - ltk_rect clip_rect = (ltk_rect){text_x, text_y, lrect.w - 2 * bw - 2 * theme.pad, text_h}; + ltk_rect clip_rect = (ltk_rect){text_x, text_y, lrect.w - 2 * bw - 2 * pad, text_h}; ltk_text_line_draw_clipped(entry->tl, draw_surf, theme.text_color, text_x - entry->cur_offset, text_y, clip_rect); if ((self->state & LTK_FOCUSED) && entry->sel_start == entry->sel_end) { int cx, cy, cw, ch; @@ -304,7 +309,9 @@ ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect static size_t xy_to_pos(ltk_entry *e, int x, int y, int snap) { - int side = theme.border_width + theme.pad; + int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(e)->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(e)->last_dpi); + int side = bw + pad; int text_w, text_h; ltk_text_line_get_size(e->tl, &text_w, &text_h); if (text_w < e->widget.lrect.w - 2 * side && ltk_text_line_get_softline_direction(e->tl, 0) == LTK_TEXT_RTL) @@ -491,15 +498,12 @@ select_all(ltk_entry *entry, ltk_key_event *event) { } static void -recalc_ideal_size(ltk_entry *entry) { +recalc_ideal_size_with_notification(ltk_entry *entry) { /* FIXME: need to react to resize and adjust cur_offset */ - int w, h; - ltk_text_line_get_size(entry->tl, &w, &h); - unsigned int ideal_h = h + 2 * theme.border_width + 2 * theme.pad; - unsigned int ideal_w = w + 2 * theme.border_width + 2 * theme.pad; - if (ideal_w != entry->widget.ideal_w || ideal_h != entry->widget.ideal_h) { - entry->widget.ideal_w = ideal_w; - entry->widget.ideal_h = ideal_h; + unsigned int old_w = entry->widget.ideal_w; + unsigned int old_h = entry->widget.ideal_h; + recalc_ideal_size(entry); + if (old_w != entry->widget.ideal_w || old_h != entry->widget.ideal_h) { if (entry->widget.parent && entry->widget.parent->vtable->child_size_change) entry->widget.parent->vtable->child_size_change(entry->widget.parent, &entry->widget); } @@ -512,7 +516,9 @@ ensure_cursor_shown(ltk_entry *entry) { /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change, and then the stuff on the next few lines is done afterwards */ /* FIXME: adjustable cursor width */ - int text_w = entry->widget.lrect.w - 2 * theme.border_width - 2 * theme.pad; + int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(entry)->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(entry)->last_dpi); + int text_w = entry->widget.lrect.w - 2 * bw - 2 * pad; if (x + 1 > text_w + entry->cur_offset) { entry->cur_offset = x - text_w + 1; entry->widget.dirty = 1; @@ -559,7 +565,7 @@ insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor) { entry->pos += reallen; entry->text[entry->len] = '\0'; ltk_text_line_set_text(entry->tl, entry->text, 0); - recalc_ideal_size(entry); + recalc_ideal_size_with_notification(entry); ensure_cursor_shown(entry); entry->widget.dirty = 1; ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget); @@ -626,7 +632,9 @@ ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) { static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) { ltk_entry *e = LTK_CAST_ENTRY(self); - int side = theme.border_width + theme.pad; + int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, self->last_dpi); + int side = bw + pad; if (event->x < side || event->x > self->lrect.w - side || event->y < side || event->y > self->lrect.h - side) { return 0; @@ -740,17 +748,33 @@ ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event) { return 0; } +static void +recalc_ideal_size(ltk_entry *entry) { + int text_w, text_h; + ltk_text_line_get_size(entry->tl, &text_w, &text_h); + int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(entry)->last_dpi); + int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(entry)->last_dpi); + entry->widget.ideal_w = text_w + bw * 2 + pad * 2; + entry->widget.ideal_h = text_h + bw * 2 + pad * 2; +} + +static void +ltk_entry_recalc_ideal_size(ltk_widget *self) { + ltk_entry *entry = LTK_CAST_ENTRY(self); + int font_size = ltk_size_to_pixel(self->window->theme->font_size, self->last_dpi); + ltk_text_line_set_font_size(entry->tl, font_size); + recalc_ideal_size(entry); +} + ltk_entry * ltk_entry_create(ltk_window *window, char *text) { ltk_entry *entry = ltk_malloc(sizeof(ltk_entry)); + ltk_fill_widget_defaults(LTK_CAST_WIDGET(entry), window, &vtable, 0, 0); + + ltk_size font_size = window->theme->font_size; + entry->tl = ltk_text_line_create_default(ltk_size_to_pixel(font_size, entry->widget.last_dpi), text, 0, -1); + recalc_ideal_size(entry); - uint16_t font_size = window->theme->font_size; - entry->tl = ltk_text_line_create_default(font_size, text, 0, -1); - int text_w, text_h; - ltk_text_line_get_size(entry->tl, &text_w, &text_h); - ltk_fill_widget_defaults(LTK_CAST_WIDGET(entry), window, &vtable, entry->widget.ideal_w, entry->widget.ideal_h); - entry->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2; - entry->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2; entry->cur_offset = 0; entry->text = ltk_strdup(text); entry->len = strlen(text); diff --git a/src/ltk/event.h b/src/ltk/event.h @@ -71,6 +71,12 @@ typedef struct { int w, h; } ltk_expose_event; +typedef struct { + ltk_event_type type; + size_t window_id; + unsigned int dpi; +} ltk_dpichange_event; + typedef union { ltk_event_type type; struct { @@ -84,12 +90,14 @@ typedef union { ltk_configure_event configure; ltk_expose_event expose; ltk_keyboard_event keyboard; + ltk_dpichange_event dpichange; } ltk_event; #include "graphics.h" #include "clipboard.h" void ltk_events_cleanup(void); +void ltk_events_init(ltk_renderdata *renderdata); /* WARNING: Text returned in key and keyboard events must be copied before calling this function again! */ int ltk_next_event(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t num_windows, ltk_clipboard *clip, size_t lang_index, ltk_event *event); void ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event); diff --git a/src/ltk/event_xlib.c b/src/ltk/event_xlib.c @@ -14,6 +14,7 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <math.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> @@ -27,6 +28,10 @@ #include <X11/extensions/XKBstr.h> #include <X11/keysym.h> +#if USE_XRANDR +#include <X11/extensions/Xrandr.h> +#endif + #include "clipboard.h" #include "clipboard_xlib.h" #include "config.h" @@ -37,6 +42,9 @@ #include "memory.h" #include "util.h" +LTK_ARRAY_INIT_FUNC_DECL_STATIC(moninfo, struct ltk_moninfo) +LTK_ARRAY_INIT_IMPL_STATIC(moninfo, struct ltk_moninfo) + #define TEXT_INITIAL_SIZE 128 static char *text = NULL; @@ -69,11 +77,16 @@ static int was_2release[] = {0, 0, 0}; used to implement double/triple-click because the actual double/triple-click/release event is generated in addition to the regular press/release */ -static int next_event_valid = 0; -static ltk_button_event next_event; static ltk_keysym get_keysym(KeySym sym); +LTK_ARRAY_INIT_DECL_STATIC(event, ltk_event) +LTK_ARRAY_INIT_IMPL_STATIC(event, ltk_event) + +/* A queue would make more sense here, but it doesn't matter + for the way it's used currently. */ +static ltk_array(event) *local_event_stack = NULL; + void ltk_events_cleanup(void) { ltk_free(text); @@ -81,6 +94,10 @@ ltk_events_cleanup(void) { cur_kbd = NULL; text = NULL; text_alloc = 0; + /* Note: This assumes that none of the events in the + stack need special cleaning. */ + if (local_event_stack) + ltk_array_destroy(event, local_event_stack); } static ltk_button_type @@ -181,17 +198,102 @@ ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event) { XkbFreeKeyboard(desc, XkbAllComponentsMask, True); } +/* FIXME: Move this to graphics_xlib.c */ +/* FIXME: maybe split renderdata into general and specific part since this could be used for all backends */ +/* returns 1 if dpi changed, 0 otherwise */ +int +ltk_recalc_renderwindow_dpi(ltk_renderwindow *window) { + ltk_renderdata *renderdata = window->renderdata; + if (!renderdata->monitors || ltk_array_len(renderdata->monitors) == 0) + return 0; + unsigned long maxarea = 0; + size_t maxindex = 0; + /* FIXME: should this use the monitor with the largest pixel area (like it + currently does) or the largest physical area? */ + for (size_t i = 0; i < ltk_array_len(renderdata->monitors); i++) { + struct ltk_moninfo *info = &ltk_array_get(renderdata->monitors, i); + ltk_rect isect = ltk_rect_intersect( + window->rect, + (ltk_rect){info->x, info->y, info->width, info->height} + ); + unsigned long tmp_area = 0; + if (isect.w > 0 && isect.h > 0 && + (tmp_area = (unsigned long)isect.w * (unsigned long)isect.h) > maxarea) { + maxarea = tmp_area; + maxindex = i; + } + } + unsigned int dpi = ltk_array_get(renderdata->monitors, maxindex).dpi; + if (dpi != window->dpi) { + window->dpi = dpi; + return 1; + } + return 0; +} + +#if USE_XRANDR +static void +update_monitor_config(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t num_windows) { + int nmon; + ltk_config *config = ltk_config_get(); + if (!config->general.mixed_dpi) + return; + XRRMonitorInfo *mi = XRRGetMonitors(renderdata->dpy, renderdata->root_window, 1, &nmon); + if (nmon > 0 && !renderdata->monitors) + renderdata->monitors = ltk_array_create(moninfo, 1); + for (int i = 0; i < nmon; i++) { + struct ltk_moninfo info = {mi[i].x, mi[i].y, mi[i].width, mi[i].height, mi[i].mwidth, mi[i].mheight, 0}; + /* FIXME: This only uses the width for the calculation. It should be the same if using + the height, but is that guaranteed? */ + /* FIXME: can width or mwidth ever by negative? */ + info.dpi = (unsigned int)round(config->general.dpi_scale * (info.width / (info.mwidth / 127.0))); + /* FIXME: need to adjust default dpi and document */ + /* -> config file dpi should still be regular dpi */ + /* FIXME: check for overflows in the later pixel computation */ + if (info.dpi == 0) + info.dpi = 10; /* at least prevent issues with zero dpi */ + if ((size_t)i < ltk_array_len(renderdata->monitors)) + ltk_array_get(renderdata->monitors, i) = info; + else + ltk_array_append(moninfo, renderdata->monitors, info); + } + XRRFreeMonitors(mi); + for (size_t i = 0; i < num_windows; i++) { + if (ltk_recalc_renderwindow_dpi(windows[i])) { + ltk_event e = (ltk_event){.dpichange = { + .type = LTK_DPICHANGE_EVENT, + .window_id = i, + .dpi = windows[i]->dpi + }}; + ltk_assert(local_event_stack != NULL); + ltk_array_append(event, local_event_stack, e); + } + } +} +#endif + #define DISTSQ(x0, y0, x1, y1) (((x1) - (x0)) * ((x1) - (x0)) + ((y1) - (y0)) * ((y1) - (y0))) + +void +ltk_events_init(ltk_renderdata *renderdata) { +#if USE_XRANDR + update_monitor_config(renderdata, NULL, 0); +#else + (void)renderdata; +#endif +} + /* return value 0 means valid event returned, 1 means no events pending, 2 means event discarded (need to call again) */ static int next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t num_windows, ltk_clipboard *clip, size_t lang_index, ltk_event *event) { + if (!local_event_stack) + local_event_stack = ltk_array_create(event, 1); /* FIXME: I guess it's technically possible that a window is destroyed between two calls - to this function, meaning that the window_id in next_event isn't correct anymore. */ - if (next_event_valid) { - next_event_valid = 0; - *event = (ltk_event){.button = next_event}; + to this function, meaning that the window_id in the local event stack isn't correct anymore. */ + if (ltk_array_len(local_event_stack) > 0) { + *event = ltk_array_pop(event, local_event_stack); return 0; } XEvent xevent; @@ -203,6 +305,17 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n ltk_generate_keyboard_event(renderdata, event); return 0; } +#if USE_XRANDR + if (xevent.xany.window == renderdata->root_window) { + if (!renderdata->xrandr_supported) + return 2; /* shouldn't normally happen */ + XRRUpdateConfiguration(&xevent); + if (xevent.type == renderdata->xrandr_event_type + RRScreenChangeNotify) { + update_monitor_config(renderdata, windows, num_windows); + } + return 2; + } +#endif *event = (ltk_event){.any = {.type = LTK_UNKNOWN_EVENT, .window_id = SIZE_MAX}}; if (XFilterEvent(&xevent, None)) return 2; @@ -228,25 +341,24 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n /* reset so normal press is sent again next time */ was_2press[button] = 0; last_button_press[button] = 0; - next_event = (ltk_button_event){ + ltk_array_append_event(local_event_stack, (ltk_event){.button = { .type = LTK_3BUTTONPRESS_EVENT, .window_id = window_id, .button = get_button(button), .x = xevent.xbutton.x, .y = xevent.xbutton.y - }; + }}); } else { was_2press[button] = 1; last_button_press[button] = xevent.xbutton.time; - next_event = (ltk_button_event){ + ltk_array_append_event(local_event_stack, (ltk_event){.button = { .type = LTK_2BUTTONPRESS_EVENT, .window_id = window_id, .button = get_button(button), .x = xevent.xbutton.x, .y = xevent.xbutton.y - }; + }}); } - next_event_valid = 1; } else { last_button_press[button] = xevent.xbutton.time; was_2press[button] = 0; @@ -298,25 +410,24 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n /* reset so normal release is sent again next time */ was_2release[button] = 0; last_button_release[button] = 0; - next_event = (ltk_button_event){ + ltk_array_append_event(local_event_stack, (ltk_event){.button = { .type = LTK_3BUTTONRELEASE_EVENT, .window_id = window_id, .button = get_button(button), .x = xevent.xbutton.x, .y = xevent.xbutton.y - }; + }}); } else { was_2release[button] = 1; last_button_release[button] = xevent.xbutton.time; - next_event = (ltk_button_event){ + ltk_array_append_event(local_event_stack, (ltk_event){.button = { .type = LTK_2BUTTONRELEASE_EVENT, .window_id = window_id, .button = get_button(button), .x = xevent.xbutton.x, .y = xevent.xbutton.y - }; + }}); } - next_event_valid = 1; } else { last_button_release[button] = xevent.xbutton.time; was_2release[button] = 0; @@ -356,7 +467,7 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n break; case ConfigureNotify: ltk_assert(window_id < num_windows); - *event = (ltk_event){.configure = { + ltk_event cevent = {.configure = { .type = LTK_CONFIGURE_EVENT, .window_id = window_id, .x = xevent.xconfigure.x, @@ -364,6 +475,20 @@ next_event_base(ltk_renderdata *renderdata, ltk_renderwindow **windows, size_t n .w = xevent.xconfigure.width, .h = xevent.xconfigure.height }}; + windows[window_id]->rect = (ltk_rect){ + xevent.xconfigure.x, xevent.xconfigure.y, + xevent.xconfigure.width, xevent.xconfigure.height + }; + if (ltk_recalc_renderwindow_dpi(windows[window_id])) { + *event = (ltk_event){.dpichange = { + .type = LTK_DPICHANGE_EVENT, + .window_id = window_id, + .dpi = windows[window_id]->dpi + }}; + ltk_array_append(event, local_event_stack, cevent); + } else { + *event = cevent; + } break; case Expose: ltk_assert(window_id < num_windows); diff --git a/src/ltk/eventdefs.h b/src/ltk/eventdefs.h @@ -41,6 +41,7 @@ typedef enum { LTK_EXPOSE_EVENT, LTK_WINDOWCLOSE_EVENT, LTK_KEYBOARDCHANGE_EVENT, + LTK_DPICHANGE_EVENT, } ltk_event_type; /* FIXME: button mask also in motion */ diff --git a/src/ltk/graphics.h b/src/ltk/graphics.h @@ -34,6 +34,15 @@ typedef enum { LTK_BORDER_ALL = 0xF } ltk_border_sides; +typedef struct { + int val; + enum { + LTK_UNIT_PX, + LTK_UNIT_PT, + LTK_UNIT_MM, + } unit; +} ltk_size; + typedef struct ltk_surface ltk_surface; /* FIXME: graphics context */ @@ -54,6 +63,38 @@ void ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect void ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints); void ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip); +/* dpi is actually 5 * the real dpi! */ +/* sz.val is scaled by 100! */ +/* FIXME: overflow prevention */ +/* -> maybe add max value for dpi and numbers that is checked beforehand */ +/* It might make sense to just use double for everything */ +static inline unsigned int +ltk_size_to_pixel(ltk_size sz, unsigned int dpi) { + if (sz.val < 0) { + switch (sz.unit) { + case LTK_UNIT_PT: + return (sz.val * dpi - 18000) / 36000; + break; + case LTK_UNIT_MM: + return (sz.val * dpi - 6300) / 12700; + break; + default: + return (sz.val - 50) / 100; + } + } else { + switch (sz.unit) { + case LTK_UNIT_PT: + return (sz.val * dpi + 18000) / 36000; + break; + case LTK_UNIT_MM: + return (sz.val * dpi + 6300) / 12700; + break; + default: + return (sz.val + 50) / 100; + } + } +} + /* TODO */ /* void ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width); @@ -65,10 +106,11 @@ void ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r /* FIXME: rename some of these functions */ void ltk_renderer_set_imspot(ltk_renderwindow *window, int x, int y); ltk_renderdata *ltk_renderer_create(void); -ltk_renderwindow *ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h); +ltk_renderwindow *ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h, unsigned int initial_dpi); void ltk_renderer_destroy_window(ltk_renderwindow *window); void ltk_renderer_destroy(ltk_renderdata *data); void ltk_renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg); +unsigned int ltk_renderer_get_window_dpi(ltk_renderwindow *window); /* FIXME: this is kind of out of place */ void ltk_renderer_swap_buffers(ltk_renderwindow *window); /* FIXME: this is just for the socket name in ltkd and is a bit weird */ diff --git a/src/ltk/graphics_xlib.c b/src/ltk/graphics_xlib.c @@ -24,12 +24,21 @@ #include <X11/extensions/Xdbe.h> #include <X11/extensions/dbe.h> +#if USE_XRANDR +#include <X11/extensions/Xrandr.h> +#endif + #include "graphics_xlib.h" #include "color.h" #include "memory.h" #include "rect.h" #include "util.h" +/* FIXME: kind of ugly to have this duplicated here and in event_xlib.c, + but I don't really want to give these functions linkage */ +LTK_ARRAY_INIT_FUNC_DECL_STATIC(moninfo, struct ltk_moninfo) +LTK_ARRAY_INIT_IMPL_STATIC(moninfo, struct ltk_moninfo) + struct ltk_surface { int w, h; ltk_renderwindow *window; @@ -484,6 +493,7 @@ ltk_renderer_create(void) { renderdata->depth = DefaultDepth(renderdata->dpy, renderdata->screen); renderdata->xkb_supported = 1; renderdata->xkb_event_type = 0; + /* FIXME: check version */ if (!XkbQueryExtension(renderdata->dpy, 0, &renderdata->xkb_event_type, NULL, &major, &minor)) { ltk_warn("XKB not supported.\n"); renderdata->xkb_supported = 0; @@ -502,11 +512,33 @@ ltk_renderer_create(void) { XkbAllStateComponentsMask, XkbGroupStateMask ); } + renderdata->monitors = NULL; + renderdata->xrandr_supported = 0; + renderdata->root_window = XRootWindow(renderdata->dpy, renderdata->screen); +#if USE_XRANDR + if (!XRRQueryVersion(renderdata->dpy, &major, &minor)) { + ltk_warn("XRandR not supported.\n"); + } else if (major < 1 || (major == 1 && minor < 5)) { + ltk_warn("X server only supports XRandR version %d.%d. Need at least 1.5.\n", major, minor); + } else { + /* FIXME: check if XRRQueryExtension allows passing NULL */ + int tmp; + if (!XRRQueryExtension(renderdata->dpy, &renderdata->xrandr_event_type, &tmp)) { + ltk_warn("Unable to get XRandR event type.\n"); + } else { + renderdata->xrandr_supported = 1; + /* FIXME: is this mask correct? */ + XRRSelectInput(renderdata->dpy, renderdata->root_window, RRScreenChangeNotifyMask); + } + } + +#endif + return renderdata; } ltk_renderwindow * -ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) { +ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h, unsigned int initial_dpi) { XSetWindowAttributes attrs; ltk_renderwindow *window = ltk_malloc(sizeof(ltk_renderwindow)); window->renderdata = data; @@ -521,6 +553,8 @@ ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | StructureNotifyMask | PointerMotionMask; + /* FIXME: conversion between signed and unsigned */ + window->rect = (ltk_rect){x, y, w, h}; /* FIXME: set border width */ window->xwindow = XCreateWindow( data->dpy, DefaultRootWindow(data->dpy), x, y, @@ -557,9 +591,17 @@ ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y XClearWindow(window->renderdata->dpy, window->xwindow); XMapRaised(window->renderdata->dpy, window->xwindow); + window->dpi = initial_dpi; + ltk_recalc_renderwindow_dpi(window); + return window; } +unsigned int +ltk_renderer_get_window_dpi(ltk_renderwindow *window) { + return window->dpi; +} + void ltk_renderer_destroy_window(ltk_renderwindow *window) { XFreeGC(window->renderdata->dpy, window->gc); @@ -572,6 +614,8 @@ ltk_renderer_destroy_window(ltk_renderwindow *window) { void ltk_renderer_destroy(ltk_renderdata *renderdata) { + if (renderdata->monitors) + ltk_array_destroy(moninfo, renderdata->monitors); XCloseDisplay(renderdata->dpy); /* FIXME: destroy visual, wm_delete_msg, etc.? */ ltk_free(renderdata); diff --git a/src/ltk/graphics_xlib.h b/src/ltk/graphics_xlib.h @@ -28,29 +28,45 @@ #include <X11/Xlib.h> #include <X11/extensions/Xdbe.h> +#include "rect.h" +#include "array.h" #include "graphics.h" #if USE_XFT == 1 #include <X11/Xft/Xft.h> #endif +struct ltk_moninfo { + int x, y; + int width, height; + int mwidth, mheight; + unsigned int dpi; +}; + +LTK_ARRAY_INIT_STRUCT_DECL(moninfo, struct ltk_moninfo) + /* FIXME: figure out which of these items might make more sense in ltk_renderwindow */ struct ltk_renderdata { Display *dpy; Visual *vis; + ltk_array(moninfo) *monitors; Colormap cm; Atom wm_delete_msg; + Window root_window; int screen; int depth; - /* double buffering enabled */ - int db_enabled; int xkb_event_type; - int xkb_supported; + int xrandr_event_type; + /* double buffering enabled */ + char db_enabled; + char xkb_supported; + char xrandr_supported; }; struct ltk_renderwindow { struct ltk_renderdata *renderdata; + unsigned int dpi; GC gc; Window xwindow; XdbeBackBuffer back_buf; @@ -59,6 +75,7 @@ struct ltk_renderwindow { XIC xic; XPoint spot; XVaNestedList spotlist; + ltk_rect rect; }; /* FIXME: only generate draw if needed */ @@ -72,6 +89,8 @@ XftDraw *ltk_surface_get_xft_draw(ltk_surface *s); Drawable ltk_surface_get_drawable(ltk_surface *s); #endif +int ltk_recalc_renderwindow_dpi(ltk_renderwindow *window); + struct ltk_color { XColor xcolor; #if USE_XFT == 1 diff --git a/src/ltk/grid.c b/src/ltk/grid.c @@ -64,6 +64,8 @@ static ltk_widget *ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *wi static ltk_widget *ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget); static ltk_widget *ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget); +static void ltk_grid_recalc_ideal_size(ltk_widget *self); + static struct ltk_widget_vtable vtable = { .draw = &ltk_grid_draw, .destroy = &ltk_grid_destroy, @@ -90,6 +92,7 @@ static struct ltk_widget_vtable vtable = { .nearest_child_right = &ltk_grid_nearest_child_right, .nearest_child_above = &ltk_grid_nearest_child_above, .nearest_child_below = &ltk_grid_nearest_child_below, + .recalc_ideal_size = &ltk_grid_recalc_ideal_size, .type = LTK_WIDGET_GRID, .flags = 0, .invalid_signal = LTK_GRID_SIGNAL_INVALID, @@ -314,6 +317,39 @@ ltk_recalculate_grid(ltk_widget *self) { } } +/* FIXME: what should be done if widget spans multiple rows/columns with static size? */ +static void +ltk_grid_recalc_ideal_size(ltk_widget *self) { + ltk_grid *grid = LTK_CAST_GRID(self); + for (int j = 0; j < grid->columns; j++) { + if (grid->column_weights[j] == 0) + grid->column_widths[j] = 0; + } + for (int i = 0; i < grid->rows; i++) { + if (grid->row_weights[i] == 0) + grid->row_heights[i] = 0; + for (int j = 0; j < grid->columns; j++) { + ltk_widget *ptr = grid->widget_grid[i * grid->columns + j]; + if (!ptr || ptr->row != i || ptr->column != j) + continue; + ltk_widget_recalc_ideal_size(ptr); + if (grid->row_weights[i] == 0 && ptr->ideal_h > (unsigned int)grid->row_heights[i]) + grid->row_heights[i] = ptr->ideal_h; + if (grid->column_weights[j] == 0 && ptr->ideal_w > (unsigned int)grid->column_widths[j]) + grid->column_widths[j] = ptr->ideal_w; + } + } + self->ideal_w = self->ideal_h = 0; + for (int i = 0; i < grid->rows; i++) { + if (grid->row_weights[i] == 0) + self->ideal_h += grid->row_heights[i]; + } + for (int j = 0; j < grid->columns; j++) { + if (grid->column_weights[j] == 0) + self->ideal_w += grid->column_widths[j]; + } +} + /* FIXME: Maybe add debug stuff to check that grid is actually parent of widget */ static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) { @@ -357,6 +393,8 @@ ltk_grid_add( ltk_assert(row >= 0 && row + row_span <= grid->rows); ltk_assert(column >= 0 && column + column_span <= grid->columns); + ltk_widget_recalc_ideal_size(widget); + widget->sticky = sticky; widget->row = row; widget->column = column; diff --git a/src/ltk/label.c b/src/ltk/label.c @@ -28,10 +28,11 @@ #include "graphics.h" #include "theme.h" -#define MAX_LABEL_PADDING 500 +#define MAX_LABEL_PADDING 50000 static void ltk_label_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); static void ltk_label_destroy(ltk_widget *self, int shallow); +static void ltk_label_recalc_ideal_size(ltk_widget *self); static struct ltk_widget_vtable vtable = { .draw = &ltk_label_draw, @@ -49,6 +50,7 @@ static struct ltk_widget_vtable vtable = { .motion_notify = NULL, .mouse_leave = NULL, .mouse_enter = NULL, + .recalc_ideal_size = &ltk_label_recalc_ideal_size, .type = LTK_WIDGET_LABEL, .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_SPECIAL, .invalid_signal = LTK_LABEL_SIGNAL_INVALID @@ -58,7 +60,7 @@ static struct { ltk_color *text_color; ltk_color *bg_color; ltk_color *bg_color_active; - int pad; + ltk_size pad; } theme; int parseinfo_sorted = 0; @@ -66,7 +68,7 @@ int parseinfo_sorted = 0; static ltk_theme_parseinfo parseinfo[] = { {"bg-color", THEME_COLOR, {.color = &theme.bg_color}, {.color = "#000000"}, 0, 0, 0}, {"bg-color-active", THEME_COLOR, {.color = &theme.bg_color_active}, {.color = "#222222"}, 0, 0, 0}, - {"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_LABEL_PADDING, 0}, + {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_LABEL_PADDING, 0}, {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0}, }; @@ -102,18 +104,31 @@ ltk_label_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect ltk_text_line_draw_clipped(label->tl, draw_surf, theme.text_color, text_x, text_y, draw_clip); } +static void +recalc_ideal_size(ltk_label *label) { + int text_w, text_h; + ltk_text_line_get_size(label->tl, &text_w, &text_h); + int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(label)->last_dpi); + label->widget.ideal_w = text_w + pad * 2; + label->widget.ideal_h = text_h + pad * 2; +} + +static void +ltk_label_recalc_ideal_size(ltk_widget *self) { + ltk_label *label = LTK_CAST_LABEL(self); + int font_size = ltk_size_to_pixel(self->window->theme->font_size, self->last_dpi); + ltk_text_line_set_font_size(label->tl, font_size); + recalc_ideal_size(label); +} + ltk_label * ltk_label_create(ltk_window *window, char *text) { ltk_label *label = ltk_malloc(sizeof(ltk_label)); - uint16_t font_size = window->theme->font_size; - label->tl = ltk_text_line_create_default(font_size, text, 0, -1); - int text_w, text_h; - ltk_text_line_get_size(label->tl, &text_w, &text_h); - /* FIXME: what was I even thinking here? label->widget.ideal_{w,h} isn't even initialized here */ - ltk_fill_widget_defaults(LTK_CAST_WIDGET(label), window, &vtable, label->widget.ideal_w, label->widget.ideal_h); - label->widget.ideal_w = text_w + theme.pad * 2; - label->widget.ideal_h = text_h + theme.pad * 2; + ltk_fill_widget_defaults(LTK_CAST_WIDGET(label), window, &vtable, 0, 0); + ltk_size font_size = window->theme->font_size; + label->tl = ltk_text_line_create_default(ltk_size_to_pixel(font_size, label->widget.last_dpi), text, 0, -1); + recalc_ideal_size(label); return label; } diff --git a/src/ltk/ltk.c b/src/ltk/ltk.c @@ -276,6 +276,7 @@ ltk_init(void) { shared_data.renderdata = ltk_renderer_create(); if (!shared_data.renderdata) return 1; /* FIXME: clean up */ + ltk_events_init(shared_data.renderdata); ltk_load_theme(theme_path); ltk_free0(theme_path); /* FIXME: maybe "general" theme instead of window theme? */ diff --git a/src/ltk/menu.c b/src/ltk/menu.c @@ -37,30 +37,26 @@ #include "graphics.h" #include "theme.h" -#define MAX_MENU_BORDER_WIDTH 100 -#define MAX_MENU_PAD 500 -#define MAX_MENU_ARROW_SIZE 100 +#define MAX_MENU_BORDER_WIDTH 10000 +#define MAX_MENU_PAD 50000 +#define MAX_MENU_ARROW_SIZE 10000 #define MAX(a, b) ((a) > (b) ? (a) : (b)) static struct theme { - int pad; - int arrow_pad; - int arrow_size; - int border_width; - int compress_borders; - ltk_color *border; ltk_color *background; ltk_color *scroll_background; ltk_color *scroll_arrow_color; + + ltk_size pad; + ltk_size arrow_pad; + ltk_size arrow_size; + ltk_size border_width; + int compress_borders; } menu_theme, submenu_theme; static struct entry_theme { - int text_pad; - int arrow_pad; - int arrow_size; - int border_width; int compress_borders; /* FIXME: should border_sides actually factor into size calculation? - probably useless and would @@ -83,6 +79,11 @@ static struct entry_theme { ltk_color *text_disabled; ltk_color *border_disabled; ltk_color *fill_disabled; + + ltk_size text_pad; + ltk_size arrow_pad; + ltk_size arrow_size; + ltk_size border_width; } menu_entry_theme, submenu_entry_theme; static void ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r); @@ -101,7 +102,6 @@ static void unpopup_active_entry(ltk_menuentry *e); static int ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event); static int ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event); static int ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event); -static void recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget); static void shrink_entries(ltk_menu *menu); static size_t get_entry(ltk_menu *menu, ltk_menuentry *entry); static void ltk_menu_destroy(ltk_widget *self, int shallow); @@ -121,7 +121,6 @@ static void ltk_menuentry_draw(ltk_widget *self, ltk_surface *s, int x, int y, l static void ltk_menuentry_destroy(ltk_widget *self, int shallow); static void ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state); static int ltk_menuentry_release(ltk_widget *self); -static void ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry); static ltk_widget *ltk_menu_prev_child(ltk_widget *self, ltk_widget *child); static ltk_widget *ltk_menu_next_child(ltk_widget *self, ltk_widget *child); @@ -129,6 +128,13 @@ static ltk_widget *ltk_menu_first_child(ltk_widget *self); static ltk_widget *ltk_menu_last_child(ltk_widget *self); static ltk_widget *ltk_menuentry_get_child(ltk_widget *self); +/* FIXME: these functions are named really badly */ +static void recalc_ideal_menu_size_with_notification(ltk_widget *self, ltk_widget *widget); +static void recalc_ideal_menu_size(ltk_menu *menu); +static void ltk_menu_recalc_ideal_size(ltk_widget *self); +static void ltk_menuentry_recalc_ideal_size(ltk_widget *self); +static void ltk_menuentry_recalc_ideal_size_with_notification(ltk_menuentry *entry); + #define IN_SUBMENU(e) (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)e->widget.parent)->is_submenu) static struct ltk_widget_vtable vtable = { @@ -146,7 +152,7 @@ static struct ltk_widget_vtable vtable = { .hide = &ltk_menu_hide, .draw = &ltk_menu_draw, .destroy = &ltk_menu_destroy, - .child_size_change = &recalc_ideal_menu_size, + .child_size_change = &recalc_ideal_menu_size_with_notification, .remove_child = &ltk_menu_remove_child, .prev_child = &ltk_menu_prev_child, .next_child = &ltk_menu_next_child, @@ -158,6 +164,7 @@ static struct ltk_widget_vtable vtable = { .nearest_child_above = &ltk_menu_nearest_child_above, .nearest_child_below = &ltk_menu_nearest_child_below, .ensure_rect_shown = &ltk_menu_ensure_rect_shown, + .recalc_ideal_size = &ltk_menu_recalc_ideal_size, .type = LTK_WIDGET_MENU, .flags = LTK_NEEDS_REDRAW, .invalid_signal = LTK_MENU_SIGNAL_INVALID, @@ -182,6 +189,7 @@ static struct ltk_widget_vtable entry_vtable = { .remove_child = &ltk_menuentry_remove_child, .first_child = &ltk_menuentry_get_child, .last_child = &ltk_menuentry_get_child, + .recalc_ideal_size = &ltk_menuentry_recalc_ideal_size, .type = LTK_WIDGET_MENUENTRY, .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE, .invalid_signal = LTK_MENUENTRY_SIGNAL_INVALID, @@ -190,10 +198,10 @@ static struct ltk_widget_vtable entry_vtable = { /* FIXME: standardize menuentry vs. menu_entry */ static ltk_theme_parseinfo menu_parseinfo[] = { - {"pad", THEME_INT, {.i = &menu_theme.pad}, {.i = 0}, 0, MAX_MENU_PAD, 0}, - {"arrow-pad", THEME_INT, {.i = &menu_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0}, - {"arrow-size", THEME_INT, {.i = &menu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0}, - {"border-width", THEME_INT, {.i = &menu_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0}, + {"pad", THEME_SIZE, {.size = &menu_theme.pad}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_PAD, 0}, + {"arrow-pad", THEME_SIZE, {.size = &menu_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0}, + {"arrow-size", THEME_SIZE, {.size = &menu_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0}, + {"border-width", THEME_SIZE, {.size = &menu_theme.border_width}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_BORDER_WIDTH, 0}, {"compress-borders", THEME_BOOL, {.b = &menu_theme.compress_borders}, {.b = 1}, 0, 0, 0}, {"border", THEME_COLOR, {.color = &menu_theme.border}, {.color = "#000000"}, 0, 0, 0}, {"background", THEME_COLOR, {.color = &menu_theme.background}, {.color = "#000000"}, 0, 0, 0}, @@ -218,10 +226,10 @@ ltk_menu_uninitialize_theme(ltk_renderdata *data) { } static ltk_theme_parseinfo menu_entry_parseinfo[] = { - {"text-pad", THEME_INT, {.i = &menu_entry_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0}, - {"arrow-pad", THEME_INT, {.i = &menu_entry_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0}, - {"arrow-size", THEME_INT, {.i = &menu_entry_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0}, - {"border-width", THEME_INT, {.i = &menu_entry_theme.border_width}, {.i = 2}, 0, MAX_MENU_BORDER_WIDTH, 0}, + {"text-pad", THEME_SIZE, {.size = &menu_entry_theme.text_pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0}, + {"arrow-pad", THEME_SIZE, {.size = &menu_entry_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0}, + {"arrow-size", THEME_SIZE, {.size = &menu_entry_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0}, + {"border-width", THEME_SIZE, {.size = &menu_entry_theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_BORDER_WIDTH, 0}, {"border-sides", THEME_BORDERSIDES, {.border = &menu_entry_theme.border_sides}, {.border = LTK_BORDER_ALL}, 0, 0, 0}, {"compress-borders", THEME_BOOL, {.b = &menu_entry_theme.compress_borders}, {.b = 1}, 0, 0, 0}, {"text", THEME_COLOR, {.color = &menu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0}, @@ -255,10 +263,10 @@ ltk_menuentry_uninitialize_theme(ltk_renderdata *data) { } static ltk_theme_parseinfo submenu_parseinfo[] = { - {"pad", THEME_INT, {.i = &submenu_theme.pad}, {.i = 0}, 0, MAX_MENU_PAD, 0}, - {"arrow-pad", THEME_INT, {.i = &submenu_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0}, - {"arrow-size", THEME_INT, {.i = &submenu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0}, - {"border-width", THEME_INT, {.i = &submenu_theme.border_width}, {.i = 1}, 0, MAX_MENU_BORDER_WIDTH, 0}, + {"pad", THEME_SIZE, {.size = &submenu_theme.pad}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_PAD, 0}, + {"arrow-pad", THEME_SIZE, {.size = &submenu_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0}, + {"arrow-size", THEME_SIZE, {.size = &submenu_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0}, + {"border-width", THEME_SIZE, {.size = &submenu_theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_BORDER_WIDTH, 0}, {"compress-borders", THEME_BOOL, {.b = &submenu_theme.compress_borders}, {.b = 1}, 0, 0, 0}, {"border", THEME_COLOR, {.color = &submenu_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0}, {"background", THEME_COLOR, {.color = &submenu_theme.background}, {.color = "#000000"}, 0, 0, 0}, @@ -283,10 +291,10 @@ ltk_submenu_uninitialize_theme(ltk_renderdata *data) { } static ltk_theme_parseinfo submenu_entry_parseinfo[] = { - {"text-pad", THEME_INT, {.i = &submenu_entry_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0}, - {"arrow-pad", THEME_INT, {.i = &submenu_entry_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0}, - {"arrow-size", THEME_INT, {.i = &submenu_entry_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0}, - {"border-width", THEME_INT, {.i = &submenu_entry_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0}, + {"text-pad", THEME_SIZE, {.size = &submenu_entry_theme.text_pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0}, + {"arrow-pad", THEME_SIZE, {.size = &submenu_entry_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0}, + {"arrow-size", THEME_SIZE, {.size = &submenu_entry_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0}, + {"border-width", THEME_SIZE, {.size = &submenu_entry_theme.border_width}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_BORDER_WIDTH, 0}, {"border-sides", THEME_BORDERSIDES, {.border = &submenu_entry_theme.border_sides}, {.border = LTK_BORDER_NONE}, 0, 0, 0}, {"compress-borders", THEME_BOOL, {.b = &submenu_entry_theme.compress_borders}, {.b = 0}, 0, 0, 0}, {"text", THEME_COLOR, {.color = &submenu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0}, @@ -356,6 +364,10 @@ ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_r ltk_menuentry *entry = LTK_CAST_MENUENTRY(self); int in_submenu = IN_SUBMENU(entry); struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme; + int bw = ltk_size_to_pixel(t->border_width, self->last_dpi); + int text_pad = ltk_size_to_pixel(t->text_pad, self->last_dpi); + int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi); + int arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi); ltk_color *text, *border, *fill; if (self->state & LTK_DISABLED) { text = t->text_disabled; @@ -383,20 +395,20 @@ ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_r int text_w, text_h; ltk_text_line_get_size(entry->text_line, &text_w, &text_h); - int text_x = x + t->text_pad + t->border_width; - int text_y = y + t->text_pad + t->border_width; + int text_x = x + text_pad + bw; + int text_y = y + text_pad + bw; ltk_rect text_clip = ltk_rect_intersect(surf_clip, (ltk_rect){text_x, text_y, text_w, text_h}); ltk_text_line_draw_clipped(entry->text_line, draw_surf, text, text_x, text_y, text_clip); if (in_submenu && entry->submenu) { ltk_point arrow_points[] = { - {x + lrect.w - t->arrow_pad - t->border_width, y + lrect.h / 2}, - {x + lrect.w - t->arrow_pad - t->border_width - t->arrow_size, y + lrect.h / 2 - t->arrow_size / 2}, - {x + lrect.w - t->arrow_pad - t->border_width - t->arrow_size, y + lrect.h / 2 + t->arrow_size / 2} + {x + lrect.w - arrow_pad - bw, y + lrect.h / 2}, + {x + lrect.w - arrow_pad - bw - arrow_size, y + lrect.h / 2 - arrow_size / 2}, + {x + lrect.w - arrow_pad - bw - arrow_size, y + lrect.h / 2 + arrow_size / 2} }; ltk_surface_fill_polygon_clipped(draw_surf, text, arrow_points, LENGTH(arrow_points), surf_clip); } - ltk_surface_draw_border_clipped(draw_surf, border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, t->border_width, t->border_sides); + ltk_surface_draw_border_clipped(draw_surf, border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, bw, t->border_sides); self->dirty = 0; } @@ -409,7 +421,12 @@ ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) { ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h}); if (clip_final.w <= 0 || clip_final.h <= 0) return; + struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme; + int bw = ltk_size_to_pixel(t->border_width, self->last_dpi); + int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi); + int arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi); + ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h}; ltk_surface_fill_rect(s, t->background, surf_clip); ltk_widget *ptr = NULL; @@ -430,39 +447,39 @@ ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) { } /* FIXME: active, pressed states */ - int sz = t->arrow_size + t->arrow_pad * 2; + int sz = arrow_size + arrow_pad * 2; int ww = self->lrect.w; int wh = self->lrect.h; int wx = x, wy = y; - int mbw = t->border_width; + int mbw = bw; /* FIXME: handle pathological case where rect is so small that this still draws outside */ /* -> this is currently a mess because some parts handle clipping properly, but the scroll arrow drawing doesn't */ if (lrect.w < (int)self->ideal_w) { ltk_surface_fill_rect(s, t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, sz, wh - mbw * 2}); ltk_surface_fill_rect(s, t->scroll_background, (ltk_rect){wx + ww - sz - mbw, wy + mbw, sz, wh - mbw * 2}); ltk_point arrow_points[3] = { - {wx + t->arrow_pad + mbw, wy + wh / 2}, - {wx + t->arrow_pad + mbw + t->arrow_size, wy + wh / 2 - t->arrow_size / 2}, - {wx + t->arrow_pad + mbw + t->arrow_size, wy + wh / 2 + t->arrow_size / 2} + {wx + arrow_pad + mbw, wy + wh / 2}, + {wx + arrow_pad + mbw + arrow_size, wy + wh / 2 - arrow_size / 2}, + {wx + arrow_pad + mbw + arrow_size, wy + wh / 2 + arrow_size / 2} }; ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3); - arrow_points[0] = (ltk_point){wx + ww - t->arrow_pad - mbw, wy + wh / 2}; - arrow_points[1] = (ltk_point){wx + ww - t->arrow_pad - mbw - t->arrow_size, wy + wh / 2 - t->arrow_size / 2}; - arrow_points[2] = (ltk_point){wx + ww - t->arrow_pad - mbw - t->arrow_size, wy + wh / 2 + t->arrow_size / 2}; + arrow_points[0] = (ltk_point){wx + ww - arrow_pad - mbw, wy + wh / 2}; + arrow_points[1] = (ltk_point){wx + ww - arrow_pad - mbw - arrow_size, wy + wh / 2 - arrow_size / 2}; + arrow_points[2] = (ltk_point){wx + ww - arrow_pad - mbw - arrow_size, wy + wh / 2 + arrow_size / 2}; ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3); } if (lrect.h < (int)self->ideal_h) { ltk_surface_fill_rect(self->window->surface, t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, ww - mbw * 2, sz}); ltk_surface_fill_rect(self->window->surface, t->scroll_background, (ltk_rect){wx + mbw, wy + wh - sz - mbw, ww - mbw * 2, sz}); ltk_point arrow_points[3] = { - {wx + ww / 2, wy + t->arrow_pad + mbw}, - {wx + ww / 2 - t->arrow_size / 2, wy + t->arrow_pad + mbw + t->arrow_size}, - {wx + ww / 2 + t->arrow_size / 2, wy + t->arrow_pad + mbw + t->arrow_size} + {wx + ww / 2, wy + arrow_pad + mbw}, + {wx + ww / 2 - arrow_size / 2, wy + arrow_pad + mbw + arrow_size}, + {wx + ww / 2 + arrow_size / 2, wy + arrow_pad + mbw + arrow_size} }; ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3); - arrow_points[0] = (ltk_point){wx + ww / 2, wy + wh - t->arrow_pad - mbw}; - arrow_points[1] = (ltk_point){wx + ww / 2 - t->arrow_size / 2, wy + wh - t->arrow_pad - mbw - t->arrow_size}; - arrow_points[2] = (ltk_point){wx + ww / 2 + t->arrow_size / 2, wy + wh - t->arrow_pad - mbw - t->arrow_size}; + arrow_points[0] = (ltk_point){wx + ww / 2, wy + wh - arrow_pad - mbw}; + arrow_points[1] = (ltk_point){wx + ww / 2 - arrow_size / 2, wy + wh - arrow_pad - mbw - arrow_size}; + arrow_points[2] = (ltk_point){wx + ww / 2 + arrow_size / 2, wy + wh - arrow_pad - mbw - arrow_size}; ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3); } ltk_surface_draw_border_clipped(s, t->border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, mbw, LTK_BORDER_ALL); @@ -485,33 +502,40 @@ ltk_menu_resize(ltk_widget *self) { struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme; struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme; + + int bw = ltk_size_to_pixel(t->border_width, self->last_dpi); + int pad = ltk_size_to_pixel(t->pad, self->last_dpi); + int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi); + int raw_arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi); + int entry_bw = ltk_size_to_pixel(et->border_width, self->last_dpi); + int ideal_w = self->ideal_w, ideal_h = self->ideal_h; - int arrow_size = t->arrow_pad * 2 + t->arrow_size; + int arrow_size = arrow_pad * 2 + raw_arrow_size; int start_x = lrect.w < ideal_w ? arrow_size : 0; int start_y = lrect.h < ideal_h ? arrow_size : 0; - start_x += t->border_width; - start_y += t->border_width; + start_x += bw; + start_y += bw; - int mbw = t->border_width; - int cur_abs_x = -(int)menu->x_scroll_offset + start_x + t->pad; - int cur_abs_y = -(int)menu->y_scroll_offset + start_y + t->pad; + int mbw = bw; + int cur_abs_x = -(int)menu->x_scroll_offset + start_x + pad; + int cur_abs_y = -(int)menu->y_scroll_offset + start_y + pad; for (size_t i = 0; i < menu->num_entries; i++) { ltk_menuentry *e = menu->entries[i]; e->widget.lrect.x = cur_abs_x; e->widget.lrect.y = cur_abs_y; if (menu->is_submenu) { - e->widget.lrect.w = ideal_w - 2 * t->pad - 2 * mbw; + e->widget.lrect.w = ideal_w - 2 * pad - 2 * mbw; e->widget.lrect.h = e->widget.ideal_h; - cur_abs_y += e->widget.ideal_h + t->pad; + cur_abs_y += e->widget.ideal_h + pad; if (et->compress_borders) - cur_abs_y -= et->border_width; + cur_abs_y -= entry_bw; } else { e->widget.lrect.w = e->widget.ideal_w; - e->widget.lrect.h = ideal_h - 2 * t->pad - 2 * mbw; - cur_abs_x += e->widget.ideal_w + t->pad; + e->widget.lrect.h = ideal_h - 2 * pad - 2 * mbw; + cur_abs_x += e->widget.ideal_w + pad; if (et->compress_borders) - cur_abs_x -= et->border_width; + cur_abs_x -= entry_bw; } e->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, e->widget.lrect); } @@ -523,7 +547,10 @@ static void ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r) { ltk_menu *menu = LTK_CAST_MENU(self); struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme; - int extra_size = theme->arrow_size + theme->arrow_pad * 2 + theme->border_width; + int bw = ltk_size_to_pixel(theme->border_width, self->last_dpi); + int arrow_pad = ltk_size_to_pixel(theme->arrow_pad, self->last_dpi); + int arrow_size = ltk_size_to_pixel(theme->arrow_size, self->last_dpi); + int extra_size = arrow_size + arrow_pad * 2 + bw; int delta = 0; if (self->lrect.w < (int)self->ideal_w && !menu->is_submenu) { if (r.x + r.w > self->lrect.w - extra_size && r.w <= self->lrect.w - 2 * extra_size) @@ -545,7 +572,9 @@ ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r) { static void ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret) { struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme; - int extra_size = theme->arrow_size * 2 + theme->arrow_pad * 4; + int arrow_pad = ltk_size_to_pixel(theme->arrow_pad, LTK_CAST_WIDGET(menu)->last_dpi); + int arrow_size = ltk_size_to_pixel(theme->arrow_size, LTK_CAST_WIDGET(menu)->last_dpi); + int extra_size = arrow_size * 2 + arrow_pad * 4; *x_ret = 0; *y_ret = 0; if (menu->widget.lrect.w < (int)menu->widget.ideal_w) { @@ -614,8 +643,10 @@ static ltk_widget * ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) { ltk_menu *menu = LTK_CAST_MENU(self); struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme; - int arrow_size = t->arrow_size + t->arrow_pad * 2; - int mbw = t->border_width; + int mbw = ltk_size_to_pixel(t->border_width, self->last_dpi); + int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi); + int raw_arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi); + int arrow_size = raw_arrow_size + arrow_pad * 2; int start_x = mbw, end_x = self->lrect.w - mbw; int start_y = mbw, end_y = self->lrect.h - mbw; if (self->lrect.w < (int)self->ideal_w) { @@ -645,7 +676,9 @@ set_scroll_timer(ltk_menu *menu, int x, int y) { return 0; int t = 0, b = 0, l = 0,r = 0; struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme; - int arrow_size = theme->arrow_size + theme->arrow_pad * 2; + int arrow_pad = ltk_size_to_pixel(theme->arrow_pad, LTK_CAST_WIDGET(menu)->last_dpi); + int raw_arrow_size = ltk_size_to_pixel(theme->arrow_size, LTK_CAST_WIDGET(menu)->last_dpi); + int arrow_size = raw_arrow_size + arrow_pad * 2; if (menu->widget.lrect.w < (int)menu->widget.ideal_w) { if (x < arrow_size) l = 1; @@ -706,6 +739,7 @@ ltk_menu_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) { return 1; } +/* FIXME: make sure timers are always removed when popups are removed */ static void ltk_menu_hide(ltk_widget *self) { ltk_menu *menu = LTK_CAST_MENU(self); @@ -730,10 +764,10 @@ popup_active_menu(ltk_menuentry *e) { return; int in_submenu = 0, was_opened_left = 0; ltk_rect menu_rect = e->widget.lrect; - ltk_point entry_global = ltk_widget_pos_to_global(&e->widget, 0, 0); + ltk_point entry_global = ltk_widget_pos_to_global(LTK_CAST_WIDGET(e), 0, 0); ltk_point menu_global; if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) { - ltk_menu *menu = (ltk_menu *)e->widget.parent; + ltk_menu *menu = LTK_CAST_MENU(e->widget.parent); in_submenu = menu->is_submenu; was_opened_left = menu->was_opened_left; menu_rect = menu->widget.lrect; @@ -744,17 +778,22 @@ popup_active_menu(ltk_menuentry *e) { int win_w = e->widget.window->rect.w; int win_h = e->widget.window->rect.h; ltk_menu *submenu = e->submenu; + ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(submenu)); + ltk_widget_resize(LTK_CAST_WIDGET(submenu)); int ideal_w = submenu->widget.ideal_w; int ideal_h = submenu->widget.ideal_h; int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h; + int submenu_bw = ltk_size_to_pixel(submenu_theme.border_width, LTK_CAST_WIDGET(e)->last_dpi); + int submenu_pad = ltk_size_to_pixel(submenu_theme.pad, LTK_CAST_WIDGET(e)->last_dpi); + int menu_bw = ltk_size_to_pixel(menu_theme.border_width, LTK_CAST_WIDGET(e)->last_dpi); if (in_submenu) { int space_left = menu_global.x; int space_right = win_w - (menu_global.x + menu_rect.w); int x_right = menu_global.x + menu_rect.w; int x_left = menu_global.x - ideal_w; if (submenu_theme.compress_borders) { - x_right -= submenu_theme.border_width; - x_left += submenu_theme.border_width; + x_right -= submenu_bw; + x_left += submenu_bw; } if (was_opened_left) { if (x_left >= 0) { @@ -786,7 +825,7 @@ popup_active_menu(ltk_menuentry *e) { } } /* subtract padding and border width so the actual entries are at the right position */ - y_final = entry_global.y - submenu_theme.pad - submenu_theme.border_width; + y_final = entry_global.y - submenu_pad - submenu_bw; if (y_final + ideal_h > win_h) y_final = win_h - ideal_h; if (y_final < 0) { @@ -799,8 +838,8 @@ popup_active_menu(ltk_menuentry *e) { int y_top = menu_global.y - ideal_h; int y_bottom = menu_global.y + menu_rect.h; if (menu_theme.compress_borders) { - y_top += menu_theme.border_width; - y_bottom -= menu_theme.border_width; + y_top += menu_bw; + y_bottom -= menu_bw; } if (space_top > space_bottom) { y_final = y_top; @@ -875,12 +914,20 @@ ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event) { return 1; } +static void +ltk_menu_recalc_ideal_size(ltk_widget *self) { + ltk_menu *menu = LTK_CAST_MENU(self); + recalc_ideal_menu_size(menu); +} + static ltk_menu * ltk_menu_create_base(ltk_window *window, int is_submenu) { ltk_menu *menu = ltk_malloc(sizeof(ltk_menu)); - menu->widget.ideal_w = menu_theme.pad; - menu->widget.ideal_h = menu_theme.pad; - ltk_fill_widget_defaults(&menu->widget, window, &vtable, menu->widget.ideal_w, menu->widget.ideal_h); + ltk_fill_widget_defaults(LTK_CAST_WIDGET(menu), window, &vtable, 0, 0); + + int menu_pad = ltk_size_to_pixel(menu_theme.pad, LTK_CAST_WIDGET(menu)->last_dpi); + menu->widget.ideal_w = menu_pad; + menu->widget.ideal_h = menu_pad; menu->widget.dirty = 1; menu->entries = NULL; @@ -896,7 +943,7 @@ ltk_menu_create_base(ltk_window *window, int is_submenu) { menu->unpopup_submenus_on_hide = 1; /* FIXME: hide widget by default so recalc doesn't cause unnecessary redrawing */ - recalc_ideal_menu_size(&menu->widget, NULL); + recalc_ideal_menu_size(menu); return menu; } @@ -930,53 +977,82 @@ insert_entry(ltk_menu *menu, ltk_menuentry *e, size_t idx) { } static void -recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget) { - ltk_menu *menu = LTK_CAST_MENU(self); - /* If widget with size change is submenu, it doesn't affect this menu */ - if (widget && widget->vtable->type == LTK_WIDGET_MENU) { - ltk_widget_resize(widget); - return; - } +recalc_ideal_menu_size(ltk_menu *menu) { struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme; struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme; - unsigned int old_w = menu->widget.ideal_w, old_h = menu->widget.ideal_h; - menu->widget.ideal_w = menu->widget.ideal_h = t->pad + t->border_width * 2; + + int bw = ltk_size_to_pixel(t->border_width, LTK_CAST_WIDGET(menu)->last_dpi); + int pad = ltk_size_to_pixel(t->pad, LTK_CAST_WIDGET(menu)->last_dpi); + int entry_bw = ltk_size_to_pixel(et->border_width, LTK_CAST_WIDGET(menu)->last_dpi); + + menu->widget.ideal_w = menu->widget.ideal_h = pad + bw * 2; ltk_menuentry *e; for (size_t i = 0; i < menu->num_entries; i++) { e = menu->entries[i]; + ltk_widget_recalc_ideal_size(LTK_CAST_WIDGET(e)); if (menu->is_submenu) { - menu->widget.ideal_w = MAX((t->pad + t->border_width) * 2 + (int)e->widget.ideal_w, (int)menu->widget.ideal_w); - menu->widget.ideal_h += e->widget.ideal_h + t->pad; + menu->widget.ideal_w = MAX((pad + bw) * 2 + (int)e->widget.ideal_w, (int)menu->widget.ideal_w); + menu->widget.ideal_h += e->widget.ideal_h + pad; if (et->compress_borders && i != 0) - menu->widget.ideal_h -= et->border_width; + menu->widget.ideal_h -= entry_bw; } else { - menu->widget.ideal_w += e->widget.ideal_w + t->pad; + menu->widget.ideal_w += e->widget.ideal_w + pad; if (et->compress_borders && i != 0) - menu->widget.ideal_w -= et->border_width; - menu->widget.ideal_h = MAX((t->pad + t->border_width) * 2 + (int)e->widget.ideal_h, (int)menu->widget.ideal_h); + menu->widget.ideal_w -= entry_bw; + menu->widget.ideal_h = MAX((pad + bw) * 2 + (int)e->widget.ideal_h, (int)menu->widget.ideal_h); } } - if ((old_w != menu->widget.ideal_w || old_h != menu->widget.ideal_h) && - menu->widget.parent && menu->widget.parent->vtable->child_size_change) { - menu->widget.parent->vtable->child_size_change(menu->widget.parent, (ltk_widget *)menu); +} + +/* FIXME: rename these functions */ +static void +recalc_ideal_menu_size_with_notification(ltk_widget *self, ltk_widget *widget) { + ltk_menu *menu = LTK_CAST_MENU(self); + /* If widget with size change is submenu, it doesn't affect this menu */ + if (widget && widget->vtable->type == LTK_WIDGET_MENU) { + ltk_widget_resize(widget); + return; + } + unsigned int old_w = self->ideal_w, old_h = self->ideal_h; + recalc_ideal_menu_size(menu); + if ((old_w != self->ideal_w || old_h != self->ideal_h) && + self->parent && self->parent->vtable->child_size_change) { + self->parent->vtable->child_size_change(self->parent, self); } else { ltk_menu_resize(self); } - menu->widget.dirty = 1; - if (!menu->widget.hidden) - ltk_window_invalidate_widget_rect(menu->widget.window, &menu->widget); + self->dirty = 1; + if (!self->hidden) + ltk_window_invalidate_widget_rect(self->window, self); } static void -ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry) { +recalc_ideal_menuentry_size(ltk_menuentry *entry) { int in_submenu = IN_SUBMENU(entry); struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme; - int extra_size = (in_submenu && entry->submenu) ? t->arrow_pad * 2 + t->arrow_size : 0; + int bw = ltk_size_to_pixel(t->border_width, LTK_CAST_WIDGET(entry)->last_dpi); + int text_pad = ltk_size_to_pixel(t->text_pad, LTK_CAST_WIDGET(entry)->last_dpi); + int arrow_pad = ltk_size_to_pixel(t->arrow_pad, LTK_CAST_WIDGET(entry)->last_dpi); + int arrow_size = ltk_size_to_pixel(t->arrow_size, LTK_CAST_WIDGET(entry)->last_dpi); + int extra_size = (in_submenu && entry->submenu) ? arrow_pad * 2 + arrow_size : 0; int text_w, text_h; ltk_text_line_get_size(entry->text_line, &text_w, &text_h); - entry->widget.ideal_w = text_w + extra_size + (t->text_pad + t->border_width) * 2; - entry->widget.ideal_h = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2; + entry->widget.ideal_w = text_w + extra_size + (text_pad + bw) * 2; + entry->widget.ideal_h = MAX(text_h + text_pad * 2, extra_size) + bw * 2; +} + +static void +ltk_menuentry_recalc_ideal_size(ltk_widget *self) { + ltk_menuentry *entry = LTK_CAST_MENUENTRY(self); + int font_size = ltk_size_to_pixel(self->window->theme->font_size, self->last_dpi); + ltk_text_line_set_font_size(entry->text_line, font_size); + recalc_ideal_menuentry_size(entry); +} + +static void +ltk_menuentry_recalc_ideal_size_with_notification(ltk_menuentry *entry) { + recalc_ideal_menuentry_size(entry); /* FIXME: only call if something changed */ if (entry->widget.parent && entry->widget.parent->vtable->child_size_change) { entry->widget.parent->vtable->child_size_change(entry->widget.parent, (ltk_widget *)entry); @@ -986,14 +1062,16 @@ ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry) { ltk_menuentry * ltk_menuentry_create(ltk_window *window, const char *text) { ltk_menuentry *e = ltk_malloc(sizeof(ltk_menuentry)); + ltk_fill_widget_defaults(&e->widget, window, &entry_vtable, 0, 0); /* FIXME: this should be split up into two versions with const or not const (one for taking over text) */ - e->text_line = ltk_text_line_create_default(window->theme->font_size, (char *)text, 0, -1); + e->text_line = ltk_text_line_create_default( + ltk_size_to_pixel(window->theme->font_size, e->widget.last_dpi), (char *)text, 0, -1 + ); e->submenu = NULL; - ltk_fill_widget_defaults(&e->widget, window, &entry_vtable, 5, 5); /* Note: This is only set as a dummy value! The actual ideal size can't be set until it is part of a menu because it needs to know which theme it should use */ - ltk_menuentry_recalc_ideal_size(e); + recalc_ideal_menuentry_size(e); e->widget.dirty = 1; return e; } @@ -1005,7 +1083,7 @@ ltk_menuentry_remove_child(ltk_widget *self, ltk_widget *widget) { return 1; widget->parent = NULL; e->submenu = NULL; - ltk_menuentry_recalc_ideal_size(e); + ltk_menuentry_recalc_ideal_size_with_notification(e); return 0; } @@ -1031,8 +1109,8 @@ ltk_menu_insert_entry(ltk_menu *menu, ltk_menuentry *entry, size_t idx) { if (insert_entry(menu, entry, idx)) return 2; /* invalid index */ entry->widget.parent = &menu->widget; - ltk_menuentry_recalc_ideal_size(entry); - recalc_ideal_menu_size(&menu->widget, NULL); + ltk_menuentry_recalc_ideal_size_with_notification(entry); + recalc_ideal_menu_size_with_notification(LTK_CAST_WIDGET(menu), NULL); menu->widget.dirty = 1; return 0; } @@ -1050,7 +1128,7 @@ ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu) { else if (e->submenu) return 2; /* entry already contains submenu */ e->submenu = submenu; - ltk_menuentry_recalc_ideal_size(e); + ltk_menuentry_recalc_ideal_size_with_notification(e); e->widget.dirty = 1; if (submenu) { submenu->widget.hidden = 1; @@ -1077,7 +1155,7 @@ ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx) { if (idx >= menu->num_entries) return 1; /* invalid index */ menu->entries[idx]->widget.parent = NULL; - ltk_menuentry_recalc_ideal_size(menu->entries[idx]); + ltk_menuentry_recalc_ideal_size_with_notification(menu->entries[idx]); memmove( menu->entries + idx, menu->entries + idx + 1, @@ -1085,7 +1163,7 @@ ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx) { ); menu->num_entries--; shrink_entries(menu); - recalc_ideal_menu_size(&menu->widget, NULL); + recalc_ideal_menu_size_with_notification(LTK_CAST_WIDGET(menu), NULL); return 0; } @@ -1119,11 +1197,11 @@ void ltk_menu_remove_all_entries(ltk_menu *menu) { for (size_t i = 0; i < menu->num_entries; i++) { menu->entries[i]->widget.parent = NULL; - ltk_menuentry_recalc_ideal_size(menu->entries[i]); + ltk_menuentry_recalc_ideal_size_with_notification(menu->entries[i]); } menu->num_entries = menu->num_alloc = 0; ltk_free0(menu->entries); - recalc_ideal_menu_size(&menu->widget, NULL); + recalc_ideal_menu_size_with_notification(LTK_CAST_WIDGET(menu), NULL); } static ltk_widget * @@ -1289,7 +1367,7 @@ ltk_menuentry_detach_submenu(ltk_menuentry *e) { return 1; e->submenu->widget.parent = NULL; e->submenu = NULL; - ltk_menuentry_recalc_ideal_size(e); + ltk_menuentry_recalc_ideal_size_with_notification(e); return 0; } diff --git a/src/ltk/num.c b/src/ltk/num.c @@ -0,0 +1,295 @@ +/* Note: strtonum taken from OpenBSD: */ +/* $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $ */ +/* + * Copyright (c) 2004 Ted Unangst and Todd Miller + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* Note: strtoll taken from OpenBSD, modified to only use base 10: */ +/* $OpenBSD: strtoll.c,v 1.10 2017/07/06 16:23:11 millert Exp $ */ +/* + * Copyright (c) 1992 The Regents of the University of California. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <errno.h> +#include <limits.h> +#include <stdlib.h> +#include <sys/types.h> +#include <ctype.h> + +/* FIXME: also use locale-independent version of isspace and isdigit? */ + +/* + * Convert a string to a long long. + * + * Ignores `locale' stuff. Assumes that the upper and lower case + * alphabets and digits are each contiguous. + */ +static long long +ltk_strtoll(const char *nptr, char **endptr) { + const char *s; + long long acc, cutoff; + int c; + int neg, any, cutlim; + const int base = 10; + + /* Skip white space and pick up leading +/- sign if any. */ + s = nptr; + do { + c = (unsigned char) *s++; + } while (isspace(c)); + if (c == '-') { + neg = 1; + c = *s++; + } else { + neg = 0; + if (c == '+') + c = *s++; + } + + /* + * Compute the cutoff value between legal numbers and illegal + * numbers. That is the largest legal value, divided by the + * base. An input number that is greater than this value, if + * followed by a legal input character, is too big. One that + * is equal to this value may be valid or not; the limit + * between valid and invalid numbers is then based on the last + * digit. For instance, if the range for long longs is + * [-9223372036854775808..9223372036854775807] and the input base + * is 10, cutoff will be set to 922337203685477580 and cutlim to + * either 7 (neg==0) or 8 (neg==1), meaning that if we have + * accumulated a value > 922337203685477580, or equal but the + * next digit is > 7 (or 8), the number is too big, and we will + * return a range error. + * + * Set any if any `digits' consumed; make it negative to indicate + * overflow. + */ + cutoff = neg ? LLONG_MIN : LLONG_MAX; + cutlim = cutoff % base; + cutoff /= base; + if (neg) { + if (cutlim > 0) { + cutlim -= base; + cutoff += 1; + } + cutlim = -cutlim; + } + for (acc = 0, any = 0;; c = (unsigned char) *s++) { + if (isdigit(c)) + c -= '0'; + else + break; + if (c >= base) + break; + if (any < 0) + continue; + if (neg) { + if (acc < cutoff || (acc == cutoff && c > cutlim)) { + any = -1; + acc = LLONG_MIN; + errno = ERANGE; + } else { + any = 1; + acc *= base; + acc -= c; + } + } else { + if (acc > cutoff || (acc == cutoff && c > cutlim)) { + any = -1; + acc = LLONG_MAX; + errno = ERANGE; + } else { + any = 1; + acc *= base; + acc += c; + } + } + } + if (endptr != 0) + *endptr = (char *) (any ? s - 1 : nptr); + return (acc); +} + +#define INVALID 1 +#define TOOSMALL 2 +#define TOOLARGE 3 + +long long +ltk_strtonum( + const char *numstr, long long minval, + long long maxval, const char **errstrp) { + long long ll = 0; + int error = 0; + char *ep; + struct errval { + const char *errstr; + int err; + } ev[4] = { + { NULL, 0 }, + { "invalid", EINVAL }, + { "too small", ERANGE }, + { "too large", ERANGE }, + }; + + ev[0].err = errno; + errno = 0; + if (minval > maxval) { + error = INVALID; + } else { + ll = ltk_strtoll(numstr, &ep); + if (numstr == ep || *ep != '\0') + error = INVALID; + else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) + error = TOOSMALL; + else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) + error = TOOLARGE; + } + if (errstrp != NULL) + *errstrp = ev[error].errstr; + errno = ev[error].err; + if (error) + ll = 0; + + return (ll); +} + +/* Parses values of the form integer.max2digits i.e. floating-point values, + but with at most two digits after the decimal point. If there are more + digits after the decimal point, they are ignored (yeah, that should + probably be changed). The return value is the parsed value, multiplied + by 100 so it can be represented as an integer. minval and maxval also must + be multiplied by 100. If there is a decimal point, but no digits after + it, the decimal point is still removed and the number is interpreted + as if there was no decimal point. */ +long long +ltk_strtoscalednum( + const char *numstr, long long minval, long long maxval, + char **endptr, const char **errstrp) { + long long ll = 0; + int error = 0; + char *ep = NULL; + struct errval { + const char *errstr; + int err; + } ev[4] = { + { NULL, 0 }, + { "invalid", EINVAL }, + { "too small", ERANGE }, + { "too large", ERANGE }, + }; + + ev[0].err = errno; + errno = 0; + if (minval > maxval) { + error = INVALID; + goto ret; + } + + ll = ltk_strtoll(numstr, &ep); + if (numstr == ep) { + error = INVALID; + goto ret; + } else if ((ll == LLONG_MIN && errno == ERANGE)) { + error = TOOSMALL; + goto ret; + } else if ((ll == LLONG_MAX && errno == ERANGE)) { + error = TOOLARGE; + goto ret; + } + + long long afterpoint = 0; + if (*ep == '.') { + ep++; + if (isdigit(*ep)) { + afterpoint += 10 * (*ep - '0'); + ep++; + if (isdigit(*ep)) { + afterpoint += *ep - '0'; + ep++; + } + } + /* FIXME: warn if there are any more digits */ + } + + /* FIXME: decrease code duplication */ + long long cutoff = ll < 0 ? LLONG_MIN : LLONG_MAX; + long long cutlim = cutoff % 100; + cutoff /= 100; + if (ll < 0) { + if (cutlim > 0) { + cutlim -= 100; + cutoff += 1; + } + cutlim = -cutlim; + } + if (ll < 0) { + if (ll < cutoff || (ll == cutoff && afterpoint > cutlim)) { + error = TOOSMALL; + goto ret; + } else { + ll *= 100; + ll -= afterpoint; + } + } else { + if (ll > cutoff || (ll == cutoff && afterpoint > cutlim)) { + error = TOOLARGE; + goto ret; + } else { + ll *= 100; + ll += afterpoint; + } + } + if (ll < minval) + error = TOOSMALL; + else if (ll > maxval) + error = TOOLARGE; + +ret: + if (endptr != NULL) + *endptr = ep; + if (errstrp != NULL) + *errstrp = ev[error].errstr; + errno = ev[error].err; + if (error) + ll = 0; + + return (ll); +} diff --git a/src/ltk/scrollbar.c b/src/ltk/scrollbar.c @@ -26,12 +26,13 @@ #include "theme.h" #include "eventdefs.h" -#define MAX_SCROLLBAR_WIDTH 100 /* completely arbitrary */ +#define MAX_SCROLLBAR_WIDTH 10000 /* completely arbitrary */ static void ltk_scrollbar_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip); static int ltk_scrollbar_mouse_press(ltk_widget *self, ltk_button_event *event); static int ltk_scrollbar_motion_notify(ltk_widget *self, ltk_motion_event *event); static void ltk_scrollbar_destroy(ltk_widget *self, int shallow); +static void ltk_scrollbar_recalc_ideal_size(ltk_widget *self); static struct ltk_widget_vtable vtable = { .draw = &ltk_scrollbar_draw, @@ -47,6 +48,7 @@ static struct ltk_widget_vtable vtable = { .mouse_enter = NULL, .child_size_change = NULL, .remove_child = NULL, + .recalc_ideal_size = &ltk_scrollbar_recalc_ideal_size, .type = LTK_WIDGET_SCROLLBAR, /* FIXME: need different activatable state so arrow keys don't move onto scrollbar */ .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS, @@ -54,17 +56,17 @@ static struct ltk_widget_vtable vtable = { }; static struct { - int size; /* width or height, depending on orientation */ ltk_color *bg_normal; ltk_color *bg_disabled; ltk_color *fg_normal; ltk_color *fg_active; ltk_color *fg_pressed; ltk_color *fg_disabled; + ltk_size size; /* width or height, depending on orientation */ } theme; static ltk_theme_parseinfo parseinfo[] = { - {"size", THEME_INT, {.i = &theme.size}, {.i = 15}, 0, MAX_SCROLLBAR_WIDTH, 0}, + {"size", THEME_SIZE, {.size = &theme.size}, {.size = {.val = 350, .unit = LTK_UNIT_MM}}, 0, MAX_SCROLLBAR_WIDTH, 0}, {"bg", THEME_COLOR, {.color = &theme.bg_normal}, {.color = "#000000"}, 0, 0, 0}, {"bg-disabled", THEME_COLOR, {.color = &theme.bg_disabled}, {.color = "#555555"}, 0, 0, 0}, {"fg", THEME_COLOR, {.color = &theme.fg_normal}, {.color = "#113355"}, 0, 0, 0}, @@ -123,7 +125,6 @@ handle_get_rect(ltk_scrollbar *sc) { return r; } -/* FIXME: implement clipping directly without extra surface */ static void ltk_scrollbar_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) { ltk_scrollbar *scrollbar = LTK_CAST_SCROLLBAR(self); @@ -225,6 +226,16 @@ ltk_scrollbar_motion_notify(ltk_widget *self, ltk_motion_event *event) { return 1; } +static void +ltk_scrollbar_recalc_ideal_size(ltk_widget *self) { + ltk_scrollbar *sc = LTK_CAST_SCROLLBAR(self); + int size = ltk_size_to_pixel(theme.size, self->last_dpi); + if (sc->orient == LTK_HORIZONTAL) + sc->widget.ideal_h = size; + else + sc->widget.ideal_w = size; +} + ltk_scrollbar * ltk_scrollbar_create(ltk_window *window, ltk_orientation orient) { ltk_scrollbar *sc = ltk_malloc(sizeof(ltk_scrollbar)); @@ -234,10 +245,7 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient) { sc->virtual_size = 1; sc->cur_pos = 0; sc->orient = orient; - if (orient == LTK_HORIZONTAL) - sc->widget.ideal_h = theme.size; - else - sc->widget.ideal_w = theme.size; + ltk_scrollbar_recalc_ideal_size(LTK_CAST_WIDGET(sc)); return sc; } diff --git a/src/ltk/strtonum.c b/src/ltk/strtonum.c @@ -1,67 +0,0 @@ -/* Note: Taken from OpenBSD: - * $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $ - */ - -/* - * Copyright (c) 2004 Ted Unangst and Todd Miller - * All rights reserved. - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include <errno.h> -#include <limits.h> -#include <stdlib.h> - -#define INVALID 1 -#define TOOSMALL 2 -#define TOOLARGE 3 - -long long -ltk_strtonum( - const char *numstr, long long minval, - long long maxval, const char **errstrp) { - long long ll = 0; - int error = 0; - char *ep; - struct errval { - const char *errstr; - int err; - } ev[4] = { - { NULL, 0 }, - { "invalid", EINVAL }, - { "too small", ERANGE }, - { "too large", ERANGE }, - }; - - ev[0].err = errno; - errno = 0; - if (minval > maxval) { - error = INVALID; - } else { - ll = strtoll(numstr, &ep, 10); - if (numstr == ep || *ep != '\0') - error = INVALID; - else if ((ll == LLONG_MIN && errno == ERANGE) || ll < minval) - error = TOOSMALL; - else if ((ll == LLONG_MAX && errno == ERANGE) || ll > maxval) - error = TOOLARGE; - } - if (errstrp != NULL) - *errstrp = ev[error].errstr; - errno = ev[error].err; - if (error) - ll = 0; - - return (ll); -} diff --git a/src/ltk/text.h b/src/ltk/text.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023 lumidify <nobody@lumidify.org> + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -32,6 +32,7 @@ void ltk_text_context_destroy(ltk_text_context *ctx); /* FIXME: allow to give length of text */ /* FIXME: uint16_t as size is kind of ugly (also see window theme) */ ltk_text_line *ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int take_over_text, int width); +void ltk_text_line_set_font_size(ltk_text_line *tl, uint16_t font_size); void ltk_text_line_set_width(ltk_text_line *tl, int width); void ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h); void ltk_text_line_destroy(ltk_text_line *tl); diff --git a/src/ltk/text_pango.c b/src/ltk/text_pango.c @@ -103,6 +103,14 @@ ltk_text_line_set_text(ltk_text_line *tl, char *text, int take_over_text) { pango_layout_set_text(tl->layout, tl->text, tl->len); } +void +ltk_text_line_set_font_size(ltk_text_line *tl, uint16_t font_size) { + PangoFontDescription *desc = pango_font_description_from_string(tl->ctx->default_font); + pango_font_description_set_absolute_size(desc, font_size * PANGO_SCALE); + pango_layout_set_font_description(tl->layout, desc); + pango_font_description_free(desc); +} + ltk_text_line * ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int take_over_text, int width) { ltk_text_context_init(ctx); @@ -116,10 +124,6 @@ ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int /* FIXME: does this ever return NULL? */ tl->layout = pango_layout_new(ctx->context); - PangoFontDescription *desc = pango_font_description_from_string(ctx->default_font); - pango_font_description_set_size(desc, font_size * PANGO_SCALE); - pango_layout_set_font_description(tl->layout, desc); - pango_font_description_free(desc); tl->ctx = ctx; pango_layout_set_wrap(tl->layout, PANGO_WRAP_WORD_CHAR); pango_layout_set_text(tl->layout, text, -1); @@ -127,6 +131,7 @@ ltk_text_line_create(ltk_text_context *ctx, uint16_t font_size, char *text, int ltk_text_line_set_width(tl, width * PANGO_SCALE); tl->attrs = NULL; ltk_text_line_clear_attrs(tl); + ltk_text_line_set_font_size(tl, font_size); return tl; } diff --git a/src/ltk/theme.c b/src/ltk/theme.c @@ -63,6 +63,29 @@ ltk_theme_handle_value(ltk_renderdata *renderdata, char *debug_name, const char entry->initialized = 1; } break; + case THEME_SIZE: + entry->ptr.size->unit = LTK_UNIT_PX; + char *endptr = NULL; + /* this already takes care of overflow prevention because entry->min and entry->max are int */ + entry->ptr.size->val = ltk_strtoscalednum(value, entry->min, entry->max, &endptr, &errstr); + if (errstr) { + ltk_warn("Invalid value '%s' for property '%s:%s': %s\n", value, debug_name, prop, errstr); + return 1; + } + if (*endptr == '\0') { + /* NOP */ + } else if (!strcmp(endptr, "px")) { + entry->ptr.size->unit = LTK_UNIT_PX; + } else if (!strcmp(endptr, "pt")) { + entry->ptr.size->unit = LTK_UNIT_PT; + } else if (!strcmp(endptr, "mm")) { + entry->ptr.size->unit = LTK_UNIT_MM; + } else { + ltk_warn("Invalid value '%s' for property '%s:%s'\n", value, debug_name, prop); + return 1; + } + entry->initialized = 1; + break; case THEME_STRING: /* FIXME: check if already set? */ *(entry->ptr.str) = ltk_strdup(value); @@ -128,6 +151,10 @@ ltk_theme_fill_defaults(ltk_renderdata *renderdata, char *debug_name, ltk_theme_ *(e->ptr.i) = e->defaultval.i; e->initialized = 1; break; + case THEME_SIZE: + *(e->ptr.size) = e->defaultval.size; + e->initialized = 1; + break; case THEME_STRING: *(e->ptr.str) = ltk_strdup(e->defaultval.str); e->initialized = 1; @@ -171,6 +198,7 @@ ltk_theme_uninitialize(ltk_renderdata *renderdata, ltk_theme_parseinfo *parseinf ltk_color_destroy(renderdata, *(e->ptr.color)); e->initialized = 0; break; + case THEME_SIZE: case THEME_INT: case THEME_BOOL: case THEME_BORDERSIDES: @@ -178,7 +206,6 @@ ltk_theme_uninitialize(ltk_renderdata *renderdata, ltk_theme_parseinfo *parseinf break; default: ltk_fatal("Invalid theme setting type. This should not happen.\n"); - /* TODO: ltk_assert(0); */ } } } diff --git a/src/ltk/theme.h b/src/ltk/theme.h @@ -26,7 +26,8 @@ typedef enum { THEME_COLOR, THEME_INT, THEME_BOOL, - THEME_BORDERSIDES + THEME_BORDERSIDES, + THEME_SIZE, } ltk_theme_datatype; typedef struct { @@ -40,6 +41,7 @@ typedef struct { int *i; int *b; ltk_border_sides *border; + ltk_size *size; } ptr; /* Note: The default color is also given as a string because it has to be allocated first (it is only a @@ -51,8 +53,13 @@ typedef struct { int i; int b; ltk_border_sides border; + ltk_size size; } defaultval; - int min, max; /* only for integers */ + /* FIXME: min/max doesn't make too much sense for sizes since they + can use different units, but that shouldn't matter for now because + min/max is only used as a sanity check to avoid extreme sizes or + negative sizes where that isn't allowed */ + int min, max; /* only for integers or sizes */ int initialized; } ltk_theme_parseinfo; diff --git a/src/ltk/util.h b/src/ltk/util.h @@ -24,6 +24,10 @@ long long ltk_strtonum( const char *numstr, long long minval, long long maxval, const char **errstrp ); +long long ltk_strtoscalednum( + const char *numstr, long long minval, long long maxval, + char **endptr, const char **errstrp +); char *ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret); int ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret); diff --git a/src/ltk/widget.c b/src/ltk/widget.c @@ -25,9 +25,12 @@ LTK_ARRAY_INIT_FUNC_DECL_STATIC(signal, ltk_signal_callback_info) LTK_ARRAY_INIT_IMPL_STATIC(signal, ltk_signal_callback_info) +/* FIXME: this should probably not take w and h */ void -ltk_fill_widget_defaults(ltk_widget *widget, ltk_window *window, - struct ltk_widget_vtable *vtable, int w, int h) { +ltk_fill_widget_defaults( + ltk_widget *widget, ltk_window *window, + struct ltk_widget_vtable *vtable, int w, int h +) { widget->window = window; widget->parent = NULL; @@ -57,6 +60,9 @@ ltk_fill_widget_defaults(ltk_widget *widget, ltk_window *window, widget->hidden = 0; widget->vtable_copied = 0; widget->signal_cbs = NULL; + /* FIXME: maybe set this to a dummy value here and don't initialize + ideal_w/h at all until it is actually needed? */ + widget->last_dpi = ltk_renderer_get_window_dpi(window->renderwindow); /* FIXME: null other members! */ } @@ -293,3 +299,13 @@ ltk_widget_get_editable_vtable(ltk_widget *widget) { } return widget->vtable; } + +void +ltk_widget_recalc_ideal_size(ltk_widget *widget) { + unsigned int dpi = ltk_renderer_get_window_dpi(widget->window->renderwindow); + if (dpi == widget->last_dpi) + return; + widget->last_dpi = dpi; + if (widget->vtable->recalc_ideal_size) + widget->vtable->recalc_ideal_size(widget); +} diff --git a/src/ltk/widget.h b/src/ltk/widget.h @@ -196,6 +196,7 @@ typedef struct { #define LTK_WIDGET_SIGNAL_MOUSE_LEAVE 8 #define LTK_WIDGET_SIGNAL_PRESS 9 #define LTK_WIDGET_SIGNAL_RELEASE 10 +/* FIXME: resize is currently triggered in a lot of cases where nothing is really resized */ #define LTK_WIDGET_SIGNAL_RESIZE 11 #define LTK_WIDGET_SIGNAL_HIDE 12 #define LTK_WIDGET_SIGNAL_DRAW 13 @@ -256,6 +257,7 @@ struct ltk_widget { ltk_rect lrect; /* logical rect */ unsigned int ideal_w; unsigned int ideal_h; + unsigned int last_dpi; /* maybe mask to determine quickly which callbacks are included? default signals only allowed to have one callback? */ @@ -297,6 +299,12 @@ typedef struct ltk_widget_vtable { int (*release)(struct ltk_widget *); void (*cmd_return)(struct ltk_widget *self, char *text, size_t len); + /* This is called when the DPI changes. It must not call child_size_change on + the parent or do any actual resizing (only ideal_w and ideal_h should be set), + but it should call ltk_widget_recalc_ideal_size on any child widgets. */ + /* When a widget is added to a container, the container should call + ltk_widget_recalc_ideal_size on the widget in case the DPI changed. */ + void (*recalc_ideal_size)(struct ltk_widget *); void (*resize)(struct ltk_widget *); void (*hide)(struct ltk_widget *); /* draw_surface: surface to draw it on @@ -317,6 +325,8 @@ typedef struct ltk_widget_vtable { struct ltk_widget *(*first_child)(struct ltk_widget *self); struct ltk_widget *(*last_child)(struct ltk_widget *self); + /* This is called when the ideal size of a child is changed (e.g. text is + added to a text entry). */ void (*child_size_change)(struct ltk_widget *, struct ltk_widget *); int (*remove_child)(struct ltk_widget *, struct ltk_widget *); /* x and y relative to widget's lrect! */ @@ -338,9 +348,13 @@ void ltk_fill_widget_defaults( void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state); void ltk_widget_resize(ltk_widget *widget); void ltk_widget_draw(ltk_widget *widget, ltk_surface *draw_surf, int x, int y, ltk_rect clip_rect); +/* FIXME: layout widgets need to recalculate ideal size when children change! */ +void ltk_widget_get_ideal_size(ltk_widget *widget, unsigned int *ideal_w_ret, unsigned int *ideal_h_ret); ltk_point ltk_widget_pos_to_global(ltk_widget *widget, int x, int y); ltk_point ltk_global_to_widget_pos(ltk_widget *widget, int x, int y); ltk_widget_vtable *ltk_widget_get_editable_vtable(ltk_widget *widget); +void ltk_widget_recalc_ideal_size(ltk_widget *widget); + #endif /* LTK_WIDGET_H */ diff --git a/src/ltk/window.c b/src/ltk/window.c @@ -17,6 +17,7 @@ #include <stdlib.h> #include <string.h> +#include <math.h> #include "ltk.h" #include "util.h" @@ -28,7 +29,7 @@ #include "memory.h" #include "eventdefs.h" -#define MAX_WINDOW_FONT_SIZE 200 +#define MAX_WINDOW_FONT_SIZE 20000 static void gen_widget_stack(ltk_widget *bottom); static ltk_widget *get_hover_popup(ltk_window *window, int x, int y); @@ -127,7 +128,7 @@ static ltk_theme_parseinfo theme_parseinfo[] = { {"bg", THEME_COLOR, {.color = &theme.bg}, {.color = "#000000"}, 0, 0, 0}, {"fg", THEME_COLOR, {.color = &theme.fg}, {.color = "#FFFFFF"}, 0, 0, 0}, {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0}, - {"font-size", THEME_INT, {.i = &theme.font_size}, {.i = 15}, 0, MAX_WINDOW_FONT_SIZE, 0}, + {"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, MAX_WINDOW_FONT_SIZE, 0}, }; static int theme_parseinfo_sorted = 0; @@ -583,6 +584,12 @@ ltk_window_other_event(ltk_window *window, ltk_event *event) { ltk_window_invalidate_rect(window, r); } else if (event->type == LTK_WINDOWCLOSE_EVENT) { ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, LTK_EMPTY_ARGLIST); + } else if (event->type == LTK_DPICHANGE_EVENT) { + if (window->root_widget) { + ltk_window_unregister_all_popups(window); /* easier than trying to resize them */ + ltk_widget_recalc_ideal_size(window->root_widget); + ltk_widget_resize(window->root_widget); + } } } @@ -654,15 +661,14 @@ ltk_window_unregister_all_popups(ltk_window *window) { ltk_window * ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) { ltk_window *window = ltk_malloc(sizeof(ltk_window)); - /* this is a bit weird because the window entry points to itself */ - /* the ideal width isn't needed for a window */ - ltk_fill_widget_defaults(&window->widget, window, &vtable, 0, 0); window->popups = NULL; window->popups_num = window->popups_alloc = 0; window->popups_locked = 0; - window->renderwindow = ltk_renderer_create_window(data, title, x, y, w, h); + ltk_config *config = ltk_config_get(); + unsigned int dpi = (unsigned int)round(config->general.dpi_scale * config->general.fixed_dpi); + window->renderwindow = ltk_renderer_create_window(data, title, x, y, w, h, dpi); ltk_renderer_set_window_properties(window->renderwindow, theme.bg); window->theme = &theme; @@ -684,6 +690,10 @@ ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, window->surface_cache = ltk_surface_cache_create(window->renderwindow); window->surface = ltk_surface_from_window(window->renderwindow, w, h); + /* This is a bit weird because the window entry points to itself */ + /* This needs to be called after window->renderwindow is set */ + ltk_fill_widget_defaults(&window->widget, window, &vtable, 0, 0); + return window; } diff --git a/src/ltk/window.h b/src/ltk/window.h @@ -31,7 +31,7 @@ typedef struct { int border_width; - int font_size; + ltk_size font_size; char *font; ltk_color *fg; ltk_color *bg; @@ -81,6 +81,10 @@ void ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_mot void ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget); void ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release); +/* IMPORTANT: Callers must call ltk_widget_recalc_ideal_size and ltk_widget_resize + first to take DPI changes into account. It wouldn't make sense for ltk_window_register_popup + to call these functions because the calling function usually needs to know the actual size + of the popup to determine where to place it. */ void ltk_window_register_popup(ltk_window *window, ltk_widget *popup); void ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup); void ltk_window_unregister_all_popups(ltk_window *window);