From 2782446b89f472beb0c1f9506d5b7d43a64116f2 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Fri, 11 Sep 2026 14:15:58 +0200 Subject: [PATCH] new exercise in unit 1, vocabulary training --- exercise_u1.c | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++ i18n.c | 127 +++++++++++++++++++++++ main.c | 12 +++ maturita.h | 12 +++ progress.c | 19 +++- style.css | 7 ++ unitmap.c | 93 ++++++++++++++++- 7 files changed, 539 insertions(+), 5 deletions(-) diff --git a/exercise_u1.c b/exercise_u1.c index dfd906a..b729a4d 100644 --- a/exercise_u1.c +++ b/exercise_u1.c @@ -977,3 +977,277 @@ GtkWidget *build_assign(UnitCtx *unit, const char *title, g_signal_connect(check, "clicked", G_CALLBACK(assign_check), ac); return page; } + +/* ------------------------------------------------------------------ */ +/* Exercise 14: Wörter übersetzen */ +/* ------------------------------------------------------------------ */ + +const TypedQ trans_p10[] = { + {"der Abend, -e", "večer|evening", "večer"}, + {"Guten Abend!", "dobrý večer|good evening", "Dobrý večer!"}, + {"du", "ty|you", "ty"}, + {"(Es) freut mich.", "těší mě|nice to meet you", "Těší mě."}, + {"gut", "dobrý|good", "dobrý"}, + {"Hallo!", "ahoj|hello", "Ahoj!"}, + {"heißen, er heißt", "jmenovat se|to be called|to be named", "jmenovat se"}, + {"Ich heiße Jens.", "jmenuji se jens|my name is jens", "Jmenuji se Jens."}, + {"Hi!", "ahoj|hi", "Ahoj! (neformálně)"}, + {"ich", "já|i", "já"}, + {"ihr", "vy|you", "vy"}, + {"ja", "ano|yes", "ano"}, + {"der Morgen", "ráno|morning", "ráno"}, + {"Guten Morgen!", "dobré ráno|good morning", "Dobré ráno!"}, + {"der Name, -n", "jméno|name", "jméno"}, + {"nein", "ne|no", "ne"}, + {"(er/sie/es) ist", "je|is", "je"}, + {"der Tag, -e", "den|day", "den"}, + {"Guten Tag!", "dobrý den|good day", "Dobrý den!"}, + {"und", "a|and", "a"}, + {"wer", "kdo|who", "kdo"}, + {"Wer bist du?", "kdo jsi|who are you", "Kdo jsi?"}, + {"wie", "jak|how", "jak"}, + {"Wie heißt du?", "jak se jmenuješ|what is your name", "Jak se jmenuješ?"}, + {"wir", "my|we", "my"}, +}; + +const TypedQ trans_p11[] = { + {"in", "v|in", "v"}, + {"in der Nähe (von)", "poblíž|v blízkosti|near|nearby", "poblíž, v blízkosti"}, + {"das Spiel, -e", "hra|game", "hra"}, + {"wo", "kde|where", "kde"}, + {"wohnen, er wohnt", "bydlet|to live", "bydlet"}, + {"Wo wohnst du?", "kde bydlíš|where do you live", "Kde bydlíš?"}, + {"Ich wohne in Prag.", "bydlím v praze|i live in prague", "Bydlím v Praze."}, + {"null", "nula|zero", "nula"}, + {"eins", "jedna|one", "jedna"}, + {"zwei", "dvě|two", "dvě"}, + {"drei", "tři|three", "tři"}, + {"vier", "čtyři|four", "čtyři"}, + {"fünf", "pět|five", "pět"}, + {"sechs", "šest|six", "šest"}, + {"sieben", "sedm|seven", "sedm"}, + {"acht", "osm|eight", "osm"}, + {"neun", "devět|nine", "devět"}, + {"zehn", "deset|ten", "deset"}, + {"elf", "jedenáct|eleven", "jedenáct"}, + {"zwölf", "dvanáct|twelve", "dvanáct"}, + {"dreizehn", "třináct|thirteen", "třináct"}, + {"vierzehn", "čtrnáct|fourteen", "čtrnáct"}, + {"fünfzehn", "patnáct|fifteen", "patnáct"}, + {"sechzehn", "šestnáct|sixteen", "šestnáct"}, + {"siebzehn", "sedmnáct|seventeen", "sedmnáct"}, + {"achtzehn", "osmnáct|eighteen", "osmnáct"}, + {"neunzehn", "devatenáct|nineteen", "devatenáct"}, + {"zwanzig", "dvacet|twenty", "dvacet"}, +}; + +const TypedQ trans_p12[] = { + {"alt", "starý|old", "starý"}, + {"er", "on|he", "on"}, + {"der Monat, -e", "měsíc|month", "měsíc"}, + {"sie", "ona|she", "ona"}, + {"sie", "oni|they", "oni"}, +}; + +const TypedQ trans_p13[] = { + {"aber", "ale|but", "ale"}, + {"das Alter", "věk|age", "věk"}, + {"auch", "také|also|too", "také"}, + {"aus", "z|from", "z"}, + {"die Band, -s", "skupina|band", "(hudební) skupina"}, + {"chatten, er chattet", "chatovat|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"}, + {"der Fan, -s", "fanoušek|fan", "fanoušek"}, + {"das Französisch", "francouzština|francouzsky|french", + "francouzština, francouzsky"}, + {"der Freund, -e", "kamarád|friend", "kamarád"}, + {"die Freundin, -nen", "kamarádka|friend", "kamarádka"}, + {"beste Freundin", "nejlepší kamarádka|best friend", "nejlepší kamarádka"}, + {"der Fußball", "fotbal|football", "fotbal"}, + {"Er ist Fußballfanatiker.", "je fotbalový fanatik|he is a football fanatic", + "Je fotbalový fanatik."}, + {"gehen, er geht", "jít|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."}, + {"gern", "rád|ráda|gladly", "rád/a"}, + {"die Gitarre, -n", "kytara|guitar", "kytara"}, + {"das Handy, -s", "mobil|mobile phone", "mobil"}, + {"das Hobby, -s", "koníček|hobby", "koníček"}, + {"hören, er hört", "poslouchat|to listen", "poslouchat"}, + {"Er hört gern Musik.", "rád poslouchá hudbu|he likes listening to music", + "Rád poslouchá hudbu."}, + {"im Internet surfen, er surft im Internet", + "surfovat na internetu|to surf the internet", "surfovat na internetu"}, + {"immer", "pořád|neustále|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 (aus), er kommt", "přicházet|pocházet|to come|to come from", + "přicházet, pocházet (z)"}, + {"Er kommt aus Wien.", "pochází z vídně|he comes from vienna", + "Pochází z Vídně."}, + {"das Konzert, -e", "koncert|concert", "koncert"}, + {"lernen, er lernt", "učit se|to learn", "učit se"}, + {"liegen, er liegt", "ležet|to lie", "ležet"}, + {"der Nachname, -n", "příjmení|surname|last name", "příjmení"}, + {"nicht weit von", "nedaleko od|not far from", "nedaleko od"}, + {"reisen, er reist", "cestovat|to travel", "cestovat"}, + {"schreiben, er schreibt", "psát|to write", "psát"}, + {"die Schule, -n", "škola|school", "škola"}, + {"spielen, er spielt", "hrát|to play", "hrát"}, + {"Sie spielt Gitarre.", "hraje na kytaru|she plays the guitar", + "Hraje na kytaru."}, + {"tanzen, er tanzt", "tancovat|to dance", "tancovat"}, + {"der Vorname, -n", "křestní jméno|first name", "křestní jméno"}, + {"der Wohnort, -e", "bydliště|place of residence", "bydliště"}, +}; + +const TypedQ trans_p14[] = { + {"falsch", "nesprávně|chybně|wrong|incorrect", "nesprávně, chybně"}, + {"die Flöte, -n", "flétna|flute", "flétna"}, + {"haben, er hat", "mít|to have", "mít"}, + {"das Instrument, -e", "nástroj|instrument", "(hudební) nástroj"}, + {"jetzt", "teď|now", "teď"}, + {"machen, er macht", "dělat|to do|to make", "dělat"}, + {"richtig", "správně|correct|right", "správně"}, + {"der Sport", "sport", "sport"}, + {"Sport treiben, er treibt Sport", "sportovat|to do sport", "sportovat"}, + {"das Squash", "squash", "squash"}, + {"das/der Yoga", "jóga|yoga", "jóga"}, +}; + +const TypedQ trans_p15[] = { + {"bald", "brzy|soon", "brzy"}, + {"Bis bald!", "brzy na shledanou|see you soon", "Brzy na shledanou!"}, + {"bis", "do|until", "do"}, + {"Bis zum nächsten Mal!", "do příštího setkání|until next time", + "Do příštího setkání!"}, + {"die Frau, -en", "paní|woman|mrs", "paní"}, + {"der Herr, -en", "pan|man|mr", "pan"}, + {"Mach's gut!", "měj se hezky|take care", "Měj se hezky!"}, + {"das Museum, Museen", "muzeum|museum", "muzeum"}, + {"Sie gehen ins Museum.", "jdou do muzea|they are going to the museum", + "Jdou do muzea."}, + {"schön", "hezký|beautiful|nice", "hezký"}, + {"Einen schönen Tag noch!", "hezký zbytek dne|have a nice day", + "Hezký zbytek dne!"}, + {"Servus!", "ahoj|hi", "Ahoj! (servus)"}, + {"Tschüs!", "ahoj|bye", "Ahoj! (při loučení)"}, + {"das Wiedersehen", "shledání|reunion", "shledání"}, + {"Auf Wiedersehen!", "na shledanou|goodbye", "Na shledanou!"}, +}; + +typedef struct { + const char *header; /* i18n key, e.g. "Strana 10" */ + const TypedQ *rows; + int n; +} TransSection; + +static const TransSection trans_sections[] = { + {"Strana 10", trans_p10, (int)G_N_ELEMENTS(trans_p10)}, + {"Strana 11", trans_p11, (int)G_N_ELEMENTS(trans_p11)}, + {"Strana 12", trans_p12, (int)G_N_ELEMENTS(trans_p12)}, + {"Strana 13", trans_p13, (int)G_N_ELEMENTS(trans_p13)}, + {"Strana 14", trans_p14, (int)G_N_ELEMENTS(trans_p14)}, + {"Strana 15", trans_p15, (int)G_N_ELEMENTS(trans_p15)}, +}; + +typedef struct { + UnitCtx *unit; + int ex_num; + GtkWidget *feedback; + GtkWidget **entries; + GtkWidget **trans; + int total; +} TransCtx; + +static void translate_check(GtkButton *button, gpointer data) { + TransCtx *ctx = data; + int ok = 0; + int idx = 0; + + (void)button; + + for (guint s = 0; s < G_N_ELEMENTS(trans_sections); s++) { + const TransSection *sec = &trans_sections[s]; + for (int i = 0; i < sec->n; i++, idx++) { + const gchar *txt = + gtk_editable_get_text(GTK_EDITABLE(ctx->entries[idx])); + gchar *norm = normalize_answer(txt); + gboolean good = + txt && txt[0] && answer_accepts(norm, sec->rows[i].answers); + + g_free(norm); + answer_mark(ctx->entries[idx], good); + if (good) + ok++; + } + } + + for (int i = 0; i < ctx->total; i++) + if (ctx->trans[i]) + gtk_widget_set_visible(ctx->trans[i], TRUE); + + if (ok == ctx->total) { + set_feedback(ctx->feedback, TRUE, tr("feedback_ok")); + mark_done(ctx->unit, ctx->ex_num); + } else { + set_feedback(ctx->feedback, FALSE, tr("feedback_retry")); + } +} + +GtkWidget *build_translate(UnitCtx *unit) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell(unit->page, "Vokabeltraining", + "sub_translate", "check", + &body, &feedback, &check); + TransCtx *ctx = g_new0(TransCtx, 1); + int total = 0; + int idx = 0; + + for (guint s = 0; s < G_N_ELEMENTS(trans_sections); s++) + total += trans_sections[s].n; + + ctx->unit = unit; + ctx->ex_num = unit->branch_ex; + ctx->feedback = feedback; + ctx->total = total; + ctx->entries = g_new0(GtkWidget *, total); + ctx->trans = g_new0(GtkWidget *, total); + + for (guint s = 0; s < G_N_ELEMENTS(trans_sections); s++) { + const TransSection *sec = &trans_sections[s]; + GtkWidget *hdr = gtk_label_new(NULL); + + i18n_bind(hdr, sec->header, 0); + gtk_widget_set_halign(hdr, GTK_ALIGN_START); + gtk_widget_add_css_class(hdr, "ex-sub"); + gtk_widget_set_margin_top(hdr, s > 0 ? 12 : 0); + gtk_box_append(GTK_BOX(body), hdr); + + for (int i = 0; i < sec->n; i++, idx++) { + GtkWidget *prompt = gtk_label_new(sec->rows[i].prompt); + GtkWidget *entry; + + gtk_widget_set_halign(prompt, GTK_ALIGN_START); + gtk_widget_add_css_class(prompt, "ex-prompt"); + gtk_label_set_wrap(GTK_LABEL(prompt), TRUE); + gtk_box_append(GTK_BOX(body), prompt); + + entry = gtk_entry_new(); + gtk_widget_set_hexpand(entry, FALSE); + gtk_widget_set_halign(entry, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), entry); + ctx->entries[idx] = entry; + + if (sec->rows[i].meaning) + ctx->trans[idx] = meaning_add(body, sec->rows[i].meaning); + } + } + + g_signal_connect(check, "clicked", G_CALLBACK(translate_check), ctx); + return page; +} diff --git a/i18n.c b/i18n.c index 8fa9cbf..aebcc37 100644 --- a/i18n.c +++ b/i18n.c @@ -75,6 +75,9 @@ const TrEntry tr_ui[] = { "Sort the phrases into greetings and farewells."}, {"sub_land", "Přiřaďte symboly ke správné zemi.", "Match the symbols to the correct country."}, + {"Vokabeltraining", NULL, "Vocabulary training"}, + {"sub_translate", "Přeložte tato slova – česky nebo anglicky.", + "Translate these words – in Czech or English."}, {"assign_hint", "Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině " "ji vrátíte zpět.", @@ -592,6 +595,130 @@ const TrEntry tr_content[] = { "My brother is driving to a lake."}, {"A já? Navštívím obchodní centrum.", NULL, "And me? I am going to a shopping centre."}, + /* ---- unit 1: Wörter übersetzen (page headers) ---- */ + {"Strana 10", NULL, "Page 10"}, + {"Strana 11", NULL, "Page 11"}, + {"Strana 12", NULL, "Page 12"}, + {"Strana 13", NULL, "Page 13"}, + {"Strana 14", NULL, "Page 14"}, + {"Strana 15", NULL, "Page 15"}, + /* ---- unit 1: Wörter übersetzen (word meanings) ---- */ + {"večer", NULL, "evening"}, + {"Dobrý večer!", NULL, "Good evening!"}, + {"ty", NULL, "you"}, + {"Těší mě.", NULL, "Nice to meet you."}, + {"dobrý", NULL, "good"}, + {"Ahoj!", NULL, "Hello!"}, + {"jmenovat se", NULL, "to be called"}, + {"Jmenuji se Jens.", NULL, "My name is Jens."}, + {"Ahoj! (neformálně)", NULL, "Hi! (informal)"}, + {"já", NULL, "I"}, + {"vy", NULL, "you (plural)"}, + {"ano", NULL, "yes"}, + {"ráno", NULL, "morning"}, + {"jméno", NULL, "name"}, + {"ne", NULL, "no"}, + {"je", NULL, "is"}, + {"den", NULL, "day"}, + {"Dobrý den!", NULL, "Good day!"}, + {"a", NULL, "and"}, + {"kdo", NULL, "who"}, + {"Kdo jsi?", NULL, "Who are you?"}, + {"jak", NULL, "how"}, + {"my", NULL, "we"}, + {"v", NULL, "in"}, + {"poblíž, v blízkosti", NULL, "near, in the vicinity"}, + {"hra", NULL, "game"}, + {"kde", NULL, "where"}, + {"bydlet", NULL, "to live"}, + {"Kde bydlíš?", NULL, "Where do you live?"}, + {"nula", NULL, "zero"}, + {"jedna", NULL, "one"}, + {"dvě", NULL, "two"}, + {"tři", NULL, "three"}, + {"čtyři", NULL, "four"}, + {"pět", NULL, "five"}, + {"sedm", NULL, "seven"}, + {"osm", NULL, "eight"}, + {"devět", NULL, "nine"}, + {"deset", NULL, "ten"}, + {"jedenáct", NULL, "eleven"}, + {"dvanáct", NULL, "twelve"}, + {"patnáct", NULL, "fifteen"}, + {"šestnáct", NULL, "sixteen"}, + {"osmnáct", NULL, "eighteen"}, + {"devatenáct", NULL, "nineteen"}, + {"starý", NULL, "old"}, + {"on", NULL, "he"}, + {"měsíc", NULL, "month"}, + {"ona", NULL, "she"}, + {"oni", NULL, "they"}, + {"ale", NULL, "but"}, + {"věk", NULL, "age"}, + {"také", NULL, "also"}, + {"(hudební) skupina", NULL, "(music) band"}, + {"chatovat", NULL, "to chat"}, + {"počítačová hra", NULL, "computer game"}, + {"e-mail", NULL, "email"}, + {"angličtina, anglicky", NULL, "English"}, + {"fanoušek", NULL, "fan"}, + {"francouzština, francouzsky", NULL, "French"}, + {"kamarádka", NULL, "friend (f.)"}, + {"nejlepší kamarádka", NULL, "best friend (f.)"}, + {"fotbal", NULL, "football"}, + {"Je fotbalový fanatik.", NULL, "He is a football fanatic."}, + {"jít", NULL, "to go"}, + {"Rád(a) chodím na koncerty.", NULL, "I like going to concerts."}, + {"rád/a", NULL, "gladly"}, + {"kytara", NULL, "guitar"}, + {"mobil", NULL, "mobile phone"}, + {"koníček", NULL, "hobby"}, + {"poslouchat", NULL, "to listen"}, + {"Rád poslouchá hudbu.", NULL, "He likes listening to music."}, + {"surfovat na internetu", NULL, "to surf the internet"}, + {"pořád, neustále", NULL, "always"}, + {"rok", NULL, "year"}, + {"žádný/žádná/žádné", NULL, "no / none"}, + {"klavír", NULL, "piano"}, + {"přicházet, pocházet (z)", NULL, "to come (from)"}, + {"Pochází z Vídně.", NULL, "He comes from Vienna."}, + {"koncert", NULL, "concert"}, + {"ležet", NULL, "to lie"}, + {"příjmení", NULL, "surname"}, + {"nedaleko od", NULL, "not far from"}, + {"cestovat", NULL, "to travel"}, + {"psát", NULL, "to write"}, + {"škola", NULL, "school"}, + {"hrát", NULL, "to play"}, + {"Hraje na kytaru.", NULL, "She plays the guitar."}, + {"tancovat", NULL, "to dance"}, + {"křestní jméno", NULL, "first name"}, + {"bydliště", NULL, "place of residence"}, + {"nesprávně, chybně", NULL, "wrong, incorrectly"}, + {"flétna", NULL, "flute"}, + {"(hudební) nástroj", NULL, "(musical) instrument"}, + {"teď", NULL, "now"}, + {"dělat", NULL, "to do"}, + {"správně", NULL, "correctly"}, + {"sport", NULL, "sport"}, + {"sportovat", NULL, "to do sport"}, + {"squash", NULL, "squash"}, + {"jóga", NULL, "yoga"}, + {"brzy", NULL, "soon"}, + {"Brzy na shledanou!", NULL, "See you soon!"}, + {"do", NULL, "until"}, + {"Do příštího setkání!", NULL, "Until next time!"}, + {"paní", NULL, "woman / Mrs."}, + {"pan", NULL, "man / Mr."}, + {"Měj se hezky!", NULL, "Take care!"}, + {"muzeum", NULL, "museum"}, + {"Jdou do muzea.", NULL, "They are going to the museum."}, + {"hezký", NULL, "beautiful"}, + {"Hezký zbytek dne!", NULL, "Have a nice rest of the day!"}, + {"Ahoj! (servus)", NULL, "Hi! (Servus)"}, + {"Ahoj! (při loučení)", NULL, "Bye!"}, + {"shledání", NULL, "reunion"}, + {"Na shledanou!", NULL, "Goodbye!"}, {NULL, NULL, NULL} }; diff --git a/main.c b/main.c index 9bba4f2..29d8fae 100644 --- a/main.c +++ b/main.c @@ -31,6 +31,12 @@ void unit_meta_init(void) { units[1].progress_file = PROGRESS_U2; units[2].sub_key = "unit3_sub"; units[2].progress_file = PROGRESS_U3; + + /* Off-path branch hanging above exercise 2 of unit 1. */ + units[0].has_branch = TRUE; + units[0].branch_name = "Vokabeltraining"; + units[0].branch_target = "u1vocab"; + units[0].branch_ex = MAX_UNIT_EX; } void unit_configure(int idx, const char *const *names, int n) { @@ -183,6 +189,12 @@ void activate(GtkApplication *app, gpointer user_data) { g_snprintf(name, sizeof(name), "%se%d", u->ex_tag, n); gtk_stack_add_named(main_stack, page, name); } + if (u->has_branch && u->branch_target) { + GtkWidget *branch = build_translate(u); + + if (branch) + gtk_stack_add_named(main_stack, branch, u->branch_target); + } } gtk_stack_set_visible_child_name(main_stack, "welcome"); diff --git a/maturita.h b/maturita.h index 996f7a6..8ee25a9 100644 --- a/maturita.h +++ b/maturita.h @@ -36,6 +36,7 @@ #define EX_SPAC 150.0 /* horizontal distance between bubbles */ #define EX_GAP 200.0 /* vertical space between folded rows */ #define EX_WAVE 26.0 /* wavy vertical offset of the bubbles */ +#define EX_BRANCH_LIFT 150.0 /* extra top space for an off-path branch */ #define HM_WORDS 5 #define HM_MAX_MISSES 6 #define HM_N_LETTERS 29 /* QWERTY rows + Ä/Ö/Ü (see hm_letters[]) */ @@ -116,6 +117,16 @@ typedef struct { double ex_cy[MAX_UNIT_EX]; int ex_rows, ex_cols, ex_cw, ex_ch; guint ex_idle; + /* optional off-path branch exercise (e.g. vocabulary training) */ + gboolean has_branch; + const char *branch_name; /* label under the branch bubble */ + const char *branch_target; /* stack page name */ + int branch_ex; /* done[] slot used by the branch */ + GtkWidget *branch_cell; + GtkWidget *branch_label; + GtkWidget *branch_icon; + double branch_cx; + double branch_cy; } UnitCtx; typedef struct { double r, g, b; } Rgb; typedef struct { @@ -816,6 +827,7 @@ char *verbq_german(const char *before, const char *answer, const char *after); GtkWidget *build_ex8(UnitCtx *unit); GtkWidget *build_ex12(UnitCtx *unit); +GtkWidget *build_translate(UnitCtx *unit); void assign_rebuild(AssignCtx *ac); void assign_chip_clicked(GtkButton *button, gpointer data); void assign_group_toggled(GtkToggleButton *button, gpointer data); diff --git a/progress.c b/progress.c index 0ce0b0d..5b5e5f5 100644 --- a/progress.c +++ b/progress.c @@ -6,7 +6,7 @@ void unit_save_progress(UnitCtx *u) { GKeyFile *kf = g_key_file_new(); - for (int i = 1; i <= u->n_ex; i++) { + for (int i = 1; i <= MAX_UNIT_EX; i++) { gchar key[8]; g_snprintf(key, sizeof(key), "%d", i); g_key_file_set_boolean(kf, "done", key, u->done[i]); @@ -40,7 +40,7 @@ void unit_load_progress(UnitCtx *u) { return; } - for (int i = 1; i <= u->n_ex; i++) { + for (int i = 1; i <= MAX_UNIT_EX; i++) { gchar key[8]; g_snprintf(key, sizeof(key), "%d", i); u->done[i] = g_key_file_get_boolean(kf, "done", key, NULL); @@ -92,6 +92,19 @@ void refresh_completion_ui(void) { } } + /* off-path branch marker */ + if (u->has_branch && u->branch_cell) { + if (u->done[u->branch_ex]) { + gtk_widget_add_css_class(u->branch_cell, "done"); + if (u->branch_icon) + gtk_widget_set_visible(u->branch_icon, TRUE); + } else { + gtk_widget_remove_css_class(u->branch_cell, "done"); + if (u->branch_icon) + gtk_widget_set_visible(u->branch_icon, FALSE); + } + } + /* roadmap node marker */ if (u->node) { if (all && u->n_ex > 0) { @@ -113,7 +126,7 @@ void refresh_completion_ui(void) { } void mark_done(UnitCtx *u, int n) { - if (!u || n < 1 || n > u->n_ex) + if (!u || n < 1 || n > MAX_UNIT_EX) return; u->done[n] = TRUE; refresh_completion_ui(); diff --git a/style.css b/style.css index a0808f1..6fd113b 100644 --- a/style.css +++ b/style.css @@ -344,6 +344,13 @@ windowcontrols button:not(:hover) image { .ex-bubble:focus-visible { outline: none; } +.ex-branch { + border-style: dashed; + border-color: @accent; +} +.ex-branch:hover { + border-style: solid; +} .bubble-number { font-size: 22px; font-weight: 800; diff --git a/unitmap.c b/unitmap.c index a3af874..cc1600d 100644 --- a/unitmap.c +++ b/unitmap.c @@ -9,6 +9,7 @@ void ex_layout_geometry(UnitCtx *u, double avail) { const int n = u->n_ex; const double phase = 2.0 * G_PI * 1.7 / (double)(n > 1 ? n - 1 : 1); + const double lift = u->has_branch ? EX_BRANCH_LIFT : 0.0; int rows; int r, c, i; @@ -26,7 +27,7 @@ void ex_layout_geometry(UnitCtx *u, double avail) { if (u->ex_cols < 1) u->ex_cols = 1; u->ex_cw = (int)(2.0 * EX_MX + (u->ex_cols - 1) * EX_SPAC); - u->ex_ch = (int)(2.0 * EX_MY + (rows - 1) * EX_GAP); + u->ex_ch = (int)(2.0 * EX_MY + (rows - 1) * EX_GAP + lift); for (r = 0; r < rows; r++) { int base = r * u->ex_cols; @@ -36,10 +37,16 @@ void ex_layout_geometry(UnitCtx *u, double avail) { i = base + c; int cc = fwd ? c : (u->ex_cols - 1 - c); u->ex_cx[i] = EX_MX + cc * EX_SPAC; - u->ex_cy[i] = EX_MY + r * EX_GAP + u->ex_cy[i] = EX_MY + lift + r * EX_GAP + EX_WAVE * sin((double)i * phase); } } + + /* The branch hangs straight above exercise 2 (index 1). */ + if (u->has_branch && n > 1) { + u->branch_cx = u->ex_cx[1]; + u->branch_cy = u->ex_cy[1] - EX_BRANCH_LIFT; + } } void ex_point(UnitCtx *u, double t, double *ox, double *oy) { @@ -125,6 +132,28 @@ void draw_ex_rail(GtkDrawingArea *area, cairo_t *cr, cairo_set_line_width(cr, 9); cairo_stroke(cr); } + + /* Off-path branch connector up from exercise 2. */ + if (u->has_branch && u->n_ex > 1) { + gboolean bdone = u->done[u->branch_ex]; + + cairo_new_path(cr); + cairo_move_to(cr, u->ex_cx[1], u->ex_cy[1]); + cairo_line_to(cr, u->branch_cx, u->branch_cy); + cairo_set_line_width(cr, 14); + cairo_set_source_rgb(cr, rail.r, rail.g, rail.b); + cairo_stroke(cr); + + cairo_new_path(cr); + cairo_move_to(cr, u->ex_cx[1], u->ex_cy[1]); + cairo_line_to(cr, u->branch_cx, u->branch_cy); + cairo_set_line_width(cr, 9); + if (bdone) + cairo_set_source_rgba(cr, green.r, green.g, green.b, 0.85); + else + cairo_set_source_rgb(cr, slate.r, slate.g, slate.b); + cairo_stroke(cr); + } } void ex_apply_layout(UnitCtx *u) { @@ -144,6 +173,16 @@ void ex_apply_layout(UnitCtx *u) { (int)(u->ex_cy[i] + EX_BUBBLE / 2.0 + 8.0)); } + if (u->has_branch && u->branch_cell) { + gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->branch_cell, + (int)(u->branch_cx - EX_BUBBLE / 2.0), + (int)(u->branch_cy - EX_BUBBLE / 2.0)); + if (u->branch_label) + gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->branch_label, + (int)(u->branch_cx + EX_BUBBLE / 2.0 + 10.0), + (int)(u->branch_cy - 14.0)); + } + gtk_widget_set_size_request(u->ex_fixed, u->ex_cw, u->ex_ch); gtk_widget_set_size_request(u->ex_rail, u->ex_cw, u->ex_ch); gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_rail, 0, 0); @@ -225,6 +264,43 @@ GtkWidget *make_bubble(UnitCtx *u, int n) { return btn; } +static GtkWidget *make_branch_bubble(UnitCtx *u) { + GtkWidget *btn; + GtkWidget *vbox; + GtkWidget *star; + GtkWidget *icon; + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "ex-bubble"); + gtk_widget_add_css_class(btn, "ex-branch"); + gtk_widget_set_size_request(btn, (int)EX_BUBBLE, (int)EX_BUBBLE); + gtk_widget_set_can_focus(btn, FALSE); + if (u->done[u->branch_ex]) + gtk_widget_add_css_class(btn, "done"); + + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2); + gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER); + gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER); + gtk_button_set_child(GTK_BUTTON(btn), vbox); + + star = gtk_label_new("★"); + gtk_widget_set_halign(star, GTK_ALIGN_CENTER); + gtk_widget_add_css_class(star, "bubble-number"); + gtk_box_append(GTK_BOX(vbox), star); + + icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 16); + gtk_widget_set_visible(icon, u->done[u->branch_ex]); + gtk_box_append(GTK_BOX(vbox), icon); + + u->branch_cell = btn; + u->branch_icon = icon; + + g_object_set_data_full(G_OBJECT(btn), "target", + g_strdup(u->branch_target), g_free); + g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL); + return btn; +} + GtkWidget *build_unit_page(UnitCtx *u) { GtkWidget *page; GtkWidget *scroll; @@ -293,6 +369,19 @@ GtkWidget *build_unit_page(UnitCtx *u) { gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0); } + if (u->has_branch && u->branch_target) { + GtkWidget *btn = make_branch_bubble(u); + GtkWidget *lbl = gtk_label_new(u->branch_name); + + u->branch_label = lbl; + gtk_widget_set_halign(lbl, GTK_ALIGN_START); + gtk_widget_add_css_class(lbl, "ex-label"); + gtk_widget_set_visible(lbl, TRUE); + + gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0); + gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0); + } + ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll)); va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll)); g_signal_connect(ha, "notify::page-size",