new exercise in unit 1, vocabulary training

This commit is contained in:
Martin Dvořák 2026-09-11 14:15:58 +02:00
commit 2782446b89
7 changed files with 539 additions and 5 deletions

View file

@ -977,3 +977,277 @@ GtkWidget *build_assign(UnitCtx *unit, const char *title,
g_signal_connect(check, "clicked", G_CALLBACK(assign_check), ac); g_signal_connect(check, "clicked", G_CALLBACK(assign_check), ac);
return page; 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;
}

127
i18n.c
View file

@ -75,6 +75,9 @@ const TrEntry tr_ui[] = {
"Sort the phrases into greetings and farewells."}, "Sort the phrases into greetings and farewells."},
{"sub_land", "Přiřaďte symboly ke správné zemi.", {"sub_land", "Přiřaďte symboly ke správné zemi.",
"Match the symbols to the correct country."}, "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", {"assign_hint",
"Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině " "Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině "
"ji vrátíte zpět.", "ji vrátíte zpět.",
@ -592,6 +595,130 @@ const TrEntry tr_content[] = {
"My brother is driving to a lake."}, "My brother is driving to a lake."},
{"A já? Navštívím obchodní centrum.", NULL, {"A já? Navštívím obchodní centrum.", NULL,
"And me? I am going to a shopping centre."}, "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} {NULL, NULL, NULL}
}; };

12
main.c
View file

@ -31,6 +31,12 @@ void unit_meta_init(void) {
units[1].progress_file = PROGRESS_U2; units[1].progress_file = PROGRESS_U2;
units[2].sub_key = "unit3_sub"; units[2].sub_key = "unit3_sub";
units[2].progress_file = PROGRESS_U3; 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) { 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); g_snprintf(name, sizeof(name), "%se%d", u->ex_tag, n);
gtk_stack_add_named(main_stack, page, name); 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"); gtk_stack_set_visible_child_name(main_stack, "welcome");

View file

@ -36,6 +36,7 @@
#define EX_SPAC 150.0 /* horizontal distance between bubbles */ #define EX_SPAC 150.0 /* horizontal distance between bubbles */
#define EX_GAP 200.0 /* vertical space between folded rows */ #define EX_GAP 200.0 /* vertical space between folded rows */
#define EX_WAVE 26.0 /* wavy vertical offset of the bubbles */ #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_WORDS 5
#define HM_MAX_MISSES 6 #define HM_MAX_MISSES 6
#define HM_N_LETTERS 29 /* QWERTY rows + Ä/Ö/Ü (see hm_letters[]) */ #define HM_N_LETTERS 29 /* QWERTY rows + Ä/Ö/Ü (see hm_letters[]) */
@ -116,6 +117,16 @@ typedef struct {
double ex_cy[MAX_UNIT_EX]; double ex_cy[MAX_UNIT_EX];
int ex_rows, ex_cols, ex_cw, ex_ch; int ex_rows, ex_cols, ex_cw, ex_ch;
guint ex_idle; 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; } UnitCtx;
typedef struct { double r, g, b; } Rgb; typedef struct { double r, g, b; } Rgb;
typedef struct { typedef struct {
@ -816,6 +827,7 @@ char *verbq_german(const char *before, const char *answer,
const char *after); const char *after);
GtkWidget *build_ex8(UnitCtx *unit); GtkWidget *build_ex8(UnitCtx *unit);
GtkWidget *build_ex12(UnitCtx *unit); GtkWidget *build_ex12(UnitCtx *unit);
GtkWidget *build_translate(UnitCtx *unit);
void assign_rebuild(AssignCtx *ac); void assign_rebuild(AssignCtx *ac);
void assign_chip_clicked(GtkButton *button, gpointer data); void assign_chip_clicked(GtkButton *button, gpointer data);
void assign_group_toggled(GtkToggleButton *button, gpointer data); void assign_group_toggled(GtkToggleButton *button, gpointer data);

View file

@ -6,7 +6,7 @@
void unit_save_progress(UnitCtx *u) { void unit_save_progress(UnitCtx *u) {
GKeyFile *kf = g_key_file_new(); 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]; gchar key[8];
g_snprintf(key, sizeof(key), "%d", i); g_snprintf(key, sizeof(key), "%d", i);
g_key_file_set_boolean(kf, "done", key, u->done[i]); g_key_file_set_boolean(kf, "done", key, u->done[i]);
@ -40,7 +40,7 @@ void unit_load_progress(UnitCtx *u) {
return; return;
} }
for (int i = 1; i <= u->n_ex; i++) { for (int i = 1; i <= MAX_UNIT_EX; i++) {
gchar key[8]; gchar key[8];
g_snprintf(key, sizeof(key), "%d", i); g_snprintf(key, sizeof(key), "%d", i);
u->done[i] = g_key_file_get_boolean(kf, "done", key, NULL); 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 */ /* roadmap node marker */
if (u->node) { if (u->node) {
if (all && u->n_ex > 0) { if (all && u->n_ex > 0) {
@ -113,7 +126,7 @@ void refresh_completion_ui(void) {
} }
void mark_done(UnitCtx *u, int n) { void mark_done(UnitCtx *u, int n) {
if (!u || n < 1 || n > u->n_ex) if (!u || n < 1 || n > MAX_UNIT_EX)
return; return;
u->done[n] = TRUE; u->done[n] = TRUE;
refresh_completion_ui(); refresh_completion_ui();

View file

@ -344,6 +344,13 @@ windowcontrols button:not(:hover) image {
.ex-bubble:focus-visible { .ex-bubble:focus-visible {
outline: none; outline: none;
} }
.ex-branch {
border-style: dashed;
border-color: @accent;
}
.ex-branch:hover {
border-style: solid;
}
.bubble-number { .bubble-number {
font-size: 22px; font-size: 22px;
font-weight: 800; font-weight: 800;

View file

@ -9,6 +9,7 @@
void ex_layout_geometry(UnitCtx *u, double avail) { void ex_layout_geometry(UnitCtx *u, double avail) {
const int n = u->n_ex; const int n = u->n_ex;
const double phase = 2.0 * G_PI * 1.7 / (double)(n > 1 ? n - 1 : 1); 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 rows;
int r, c, i; int r, c, i;
@ -26,7 +27,7 @@ void ex_layout_geometry(UnitCtx *u, double avail) {
if (u->ex_cols < 1) if (u->ex_cols < 1)
u->ex_cols = 1; u->ex_cols = 1;
u->ex_cw = (int)(2.0 * EX_MX + (u->ex_cols - 1) * EX_SPAC); 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++) { for (r = 0; r < rows; r++) {
int base = r * u->ex_cols; int base = r * u->ex_cols;
@ -36,10 +37,16 @@ void ex_layout_geometry(UnitCtx *u, double avail) {
i = base + c; i = base + c;
int cc = fwd ? c : (u->ex_cols - 1 - c); int cc = fwd ? c : (u->ex_cols - 1 - c);
u->ex_cx[i] = EX_MX + cc * EX_SPAC; 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); + 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) { 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_set_line_width(cr, 9);
cairo_stroke(cr); 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) { 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)); (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_fixed, u->ex_cw, u->ex_ch);
gtk_widget_set_size_request(u->ex_rail, 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); 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; 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 *build_unit_page(UnitCtx *u) {
GtkWidget *page; GtkWidget *page;
GtkWidget *scroll; GtkWidget *scroll;
@ -293,6 +369,19 @@ GtkWidget *build_unit_page(UnitCtx *u) {
gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0); 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)); ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll)); va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
g_signal_connect(ha, "notify::page-size", g_signal_connect(ha, "notify::page-size",