diff --git a/exercise_u1.c b/exercise_u1.c index dd0271b..c093f79 100644 --- a/exercise_u1.c +++ b/exercise_u1.c @@ -990,7 +990,7 @@ const TypedQ trans_p10[] = { "Těší mě."}, {"gut", "dobrý|dobře|good", "dobrý"}, {"Hallo!", "ahoj|hello", "Ahoj!"}, - {"heißen (Infinitiv), er heißt", + {"heißen (Infinitiv)", "jmenovat se|jmenuje se|to be called|to be named", "jmenovat se"}, {"Ich heiße Jens.", "jmenuji se jens|jmenuju se jens|my name is jens", "Jmenuji se Jens."}, @@ -1022,7 +1022,7 @@ const TypedQ trans_p11[] = { "poblíž, v blízkosti"}, {"das Spiel, -e", "hra|game", "hra"}, {"wo", "kde|where", "kde"}, - {"wohnen (Infinitiv), er wohnt", "bydlet|bydlí|to live", "bydlet"}, + {"wohnen (Infinitiv)", "bydlet|bydlí|to live", "bydlet"}, {"Wo wohnst du?", "kde bydlíš|kde bydlíte|where do you live", "Kde bydlíš?"}, {"Ich wohne in Prag.", "bydlím v praze|i live in prague", "Bydlím v Praze."}, @@ -1063,7 +1063,7 @@ const TypedQ trans_p13[] = { {"auch", "také|taky|also|too", "také"}, {"aus", "z|ze|from", "z"}, {"die Band, -s", "skupina|kapela|band", "(hudební) skupina"}, - {"chatten (Infinitiv), er chattet", "chatovat|chatuje|to chat", "chatovat"}, + {"chatten (Infinitiv)", "chatovat|chatuje|to chat", "chatovat"}, {"das Computerspiel, -e", "počítačová hra|computer game", "počítačová hra"}, {"die/das E-Mail, -s", "e-mail|email", "e-mail"}, {"das Englisch", "angličtina|anglicky|english", "angličtina, anglicky"}, @@ -1077,7 +1077,7 @@ const TypedQ trans_p13[] = { {"der Fußball", "fotbal|football|soccer", "fotbal"}, {"Er ist Fußballfanatiker.", "je fotbalový fanatik|he is a football fanatic", "Je fotbalový fanatik."}, - {"gehen (Infinitiv), er geht", "jít|jde|chodit|to go", "jít"}, + {"gehen (Infinitiv)", "jít|jde|chodit|to go", "jít"}, {"Ich gehe gern auf Konzerte.", "rád chodím na koncerty|ráda chodím na koncerty|i like going to concerts", "Rád(a) chodím na koncerty."}, @@ -1085,34 +1085,34 @@ const TypedQ trans_p13[] = { {"die Gitarre, -n", "kytara|guitar", "kytara"}, {"das Handy, -s", "mobil|mobilní telefon|mobile phone", "mobil"}, {"das Hobby, -s", "koníček|hobby", "koníček"}, - {"hören (Infinitiv), er hört", "poslouchat|poslouchá|slyšet|to listen|to hear", + {"hören (Infinitiv)", "poslouchat|poslouchá|slyšet|to listen|to hear", "poslouchat"}, {"Er hört gern Musik.", "rád poslouchá hudbu|he likes listening to music", "Rád poslouchá hudbu."}, - {"im Internet surfen (Infinitiv), er surft im Internet", + {"im Internet surfen (Infinitiv)", "surfovat na internetu|to surf the internet", "surfovat na internetu"}, {"immer", "pořád|neustále|vždy|vždycky|always", "pořád, neustále"}, {"das Jahr, -e", "rok|year", "rok"}, {"kein/keine/kein", "žádný|žádná|žádné|no|none", "žádný/žádná/žádné"}, {"das Klavier, -e", "klavír|piano", "klavír"}, - {"kommen (Infinitiv, aus), er kommt", + {"kommen (Infinitiv, aus)", "přicházet|přijít|přichází|pochází|to come|to come from", "přicházet, pocházet (z)"}, {"Er kommt aus Wien.", "pochází z vídně|he comes from vienna|he is from vienna", "Pochází z Vídně."}, {"das Konzert, -e", "koncert|concert", "koncert"}, - {"lernen (Infinitiv), er lernt", "učit se|učí se|to learn", "učit se"}, - {"liegen (Infinitiv), er liegt", "ležet|leží|to lie|to be located", "ležet"}, + {"lernen (Infinitiv)", "učit se|učí se|to learn", "učit se"}, + {"liegen (Infinitiv)", "ležet|leží|to lie|to be located", "ležet"}, {"der Nachname, -n", "příjmení|surname|last name", "příjmení"}, {"nicht weit von", "nedaleko od|not far from", "nedaleko od"}, - {"reisen (Infinitiv), er reist", "cestovat|cestuje|to travel", "cestovat"}, - {"schreiben (Infinitiv), er schreibt", "psát|píše|to write", "psát"}, + {"reisen (Infinitiv)", "cestovat|cestuje|to travel", "cestovat"}, + {"schreiben (Infinitiv)", "psát|píše|to write", "psát"}, {"die Schule, -n", "škola|school", "škola"}, - {"spielen (Infinitiv), er spielt", "hrát|hraje|to play", "hrát"}, + {"spielen (Infinitiv)", "hrát|hraje|to play", "hrát"}, {"Sie spielt Gitarre.", "hraje na kytaru|she plays the guitar", "Hraje na kytaru."}, - {"tanzen (Infinitiv), er tanzt", "tancovat|tančí|tancuje|to dance", "tancovat"}, + {"tanzen (Infinitiv)", "tancovat|tančí|tancuje|to dance", "tancovat"}, {"der Vorname, -n", "křestní jméno|first name", "křestní jméno"}, {"der Wohnort, -e", "bydliště|místo bydliště|place of residence", "bydliště"}, @@ -1121,13 +1121,13 @@ const TypedQ trans_p13[] = { const TypedQ trans_p14[] = { {"falsch", "nesprávně|chybně|špatně|wrong|incorrect", "nesprávně, chybně"}, {"die Flöte, -n", "flétna|flute", "flétna"}, - {"haben (Infinitiv), er hat", "mít|má|to have", "mít"}, + {"haben (Infinitiv)", "mít|má|to have", "mít"}, {"das Instrument, -e", "nástroj|instrument", "(hudební) nástroj"}, {"jetzt", "teď|nyní|now", "teď"}, - {"machen (Infinitiv), er macht", "dělat|dělá|to do|to make", "dělat"}, + {"machen (Infinitiv)", "dělat|dělá|to do|to make", "dělat"}, {"richtig", "správně|správný|correct|right", "správně"}, {"der Sport", "sport", "sport"}, - {"Sport treiben (Infinitiv), er treibt Sport", + {"Sport treiben (Infinitiv)", "sportovat|sportuje|provozovat sport|to do sport", "sportovat"}, {"das Squash", "squash", "squash"}, {"das/der Yoga", "jóga|yoga", "jóga"}, diff --git a/globals.c b/globals.c index 7251a3f..2c12b58 100644 --- a/globals.c +++ b/globals.c @@ -16,6 +16,8 @@ UiLang app_lang = LANG_CS; ThemePalette app_theme; +double app_ui_scale = 1.0; + const char *theme_names[THEME_COUNT] = { "Catppuccin", "Nord", diff --git a/main.c b/main.c index 6c2c8ee..84b16a2 100644 --- a/main.c +++ b/main.c @@ -126,6 +126,47 @@ GtkWidget *build_exercise_page(UnitCtx *u, int n) { } } +static int ui_scale_last_w = -1; +static guint ui_scale_timer = 0; +static int ui_scale_pending_w = 0; + +static gboolean ui_scale_apply(gpointer data) { + double scale; + + (void)data; + ui_scale_timer = 0; + scale = 1.0 + (ui_scale_pending_w - 760) / 1900.0; + if (scale < 1.0) + scale = 1.0; + if (scale > 1.5) + scale = 1.5; + scale = (double)((int)(scale * 20.0 + 0.5)) / 20.0; + if (scale != app_ui_scale) { + app_ui_scale = scale; + apply_theme(); + } + return G_SOURCE_REMOVE; +} + +/* Grow the interface's text as the window gets wider, so it stays comfortable + * when maximised on a large or high-DPI display. Debounced to avoid reloading + * the stylesheet on every frame while dragging. */ +static gboolean ui_scale_tick(GtkWidget *w, GdkFrameClock *clock, + gpointer data) { + int width = gtk_widget_get_allocated_width(w); + + (void)clock; + (void)data; + if (width < 200 || width == ui_scale_last_w) + return G_SOURCE_CONTINUE; + ui_scale_last_w = width; + ui_scale_pending_w = width; + if (ui_scale_timer) + g_source_remove(ui_scale_timer); + ui_scale_timer = g_timeout_add(120, ui_scale_apply, NULL); + return G_SOURCE_CONTINUE; +} + void activate(GtkApplication *app, gpointer user_data) { GtkWindow *window; GtkWidget *headerbar; @@ -243,6 +284,8 @@ void activate(GtkApplication *app, gpointer user_data) { apply_theme(); apply_language(); + gtk_widget_add_tick_callback(GTK_WIDGET(window), ui_scale_tick, NULL, NULL); + gtk_window_present(window); } diff --git a/maturita.h b/maturita.h index 427cd21..505bfdf 100644 --- a/maturita.h +++ b/maturita.h @@ -524,6 +524,7 @@ extern ThemeId app_theme_id; extern ColorMode app_color_mode; extern UiLang app_lang; extern ThemePalette app_theme; +extern double app_ui_scale; extern const char *theme_names[THEME_COUNT]; extern GtkWidget *theme_swatch_areas[THEME_COUNT]; extern UnitCtx units[NUM_UNITS]; diff --git a/theme.c b/theme.c index c8b8b93..c43d0d2 100644 --- a/theme.c +++ b/theme.c @@ -188,6 +188,42 @@ char *read_css_file(void) { return contents; } +typedef struct { + double scale; +} FontScale; + +static gboolean scale_font_size_eval(const GMatchInfo *info, GString *result, + gpointer user_data) { + const FontScale *fs = user_data; + char *space = g_match_info_fetch(info, 1); + char *num = g_match_info_fetch(info, 2); + int px = (int)g_ascii_strtoll(num, NULL, 10); + int scaled = (int)(px * fs->scale + 0.5); + + g_string_append_printf(result, "font-size:%s%dpx", space, scaled); + g_free(space); + g_free(num); + return FALSE; +} + +/* Scale every explicit `font-size: Npx` declaration so the interface grows on + * large (e.g. maximised / high-DPI) windows. Layout metrics stay untouched. */ +static char *scale_font_sizes(const char *css, double scale) { + GRegex *re; + char *out; + FontScale fs; + + if (scale <= 1.0001) + return g_strdup(css); + + fs.scale = scale; + re = g_regex_new("font-size:([ \t]*)([0-9]+)px", 0, 0, NULL); + out = g_regex_replace_eval(re, css, -1, 0, 0, + scale_font_size_eval, &fs, NULL); + g_regex_unref(re); + return out; +} + /* The palette is data-driven, so its @define-color bindings are generated * here and prepended to the rules loaded from the external stylesheet. */ char *build_theme_css(const ThemePalette *p) { @@ -217,10 +253,22 @@ char *build_theme_css(const ThemePalette *p) { p->success, p->success2, p->warning, p->error, p->on_accent, p->node, p->locked_bg, p->locked_border); char *rules = read_css_file(); - char *css = g_strconcat(palette, rules, NULL); + char *scaled = scale_font_sizes(rules, app_ui_scale); + char *base; + char *css; + + if (app_ui_scale > 1.0001) + base = g_strdup_printf("window { font-size: %dpx; }\n", + (int)(14.0 * app_ui_scale + 0.5)); + else + base = g_strdup(""); + + css = g_strconcat(palette, base, scaled, NULL); g_free(palette); g_free(rules); + g_free(scaled); + g_free(base); return css; }