Improve multiple-choice quiz layout with cards and clearer states.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-14 15:32:52 +02:00
commit 1e9c9b5f84
6 changed files with 153 additions and 105 deletions

View file

@ -78,6 +78,73 @@ GtkWidget *meaning_add(GtkWidget *body, const char *text) {
return lbl; return lbl;
} }
GtkWidget *mcq_option_toggle(const char *text, int index,
GtkToggleButton **group_first) {
GtkWidget *tb;
GtkWidget *lab;
char *labeled;
labeled = g_strdup_printf("%c %s", 'A' + index, text);
tb = gtk_toggle_button_new();
lab = gtk_label_new(labeled);
g_free(labeled);
gtk_label_set_wrap(GTK_LABEL(lab), TRUE);
gtk_label_set_wrap_mode(GTK_LABEL(lab), PANGO_WRAP_WORD_CHAR);
gtk_label_set_xalign(GTK_LABEL(lab), 0.0);
gtk_widget_set_halign(lab, GTK_ALIGN_FILL);
gtk_widget_set_hexpand(lab, TRUE);
gtk_button_set_child(GTK_BUTTON(tb), lab);
gtk_widget_add_css_class(tb, "pill");
gtk_widget_add_css_class(tb, "quiz-pill");
gtk_widget_set_hexpand(tb, TRUE);
gtk_widget_set_halign(tb, GTK_ALIGN_FILL);
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), *group_first);
if (!*group_first)
*group_first = GTK_TOGGLE_BUTTON(tb);
return tb;
}
GtkWidget *mcq_append_question(GtkWidget *parent, int num, const ChoiceQ *q,
GtkToggleButton **out_toggles) {
GtkWidget *card;
GtkWidget *prompt;
GtkWidget *opts;
GtkToggleButton *first = NULL;
char *qtext;
int o;
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_add_css_class(card, "quiz-card");
gtk_widget_set_hexpand(card, TRUE);
gtk_box_append(GTK_BOX(parent), card);
qtext = g_strdup_printf("%d.) %s", num, q->prompt);
prompt = gtk_label_new(qtext);
g_free(qtext);
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
gtk_label_set_xalign(GTK_LABEL(prompt), 0.0);
gtk_widget_add_css_class(prompt, "quiz-q");
gtk_box_append(GTK_BOX(card), prompt);
opts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_add_css_class(opts, "quiz-options");
gtk_box_append(GTK_BOX(card), opts);
for (o = 0; o < q->n_options; o++) {
GtkWidget *tb = mcq_option_toggle(q->options[o], o, &first);
gtk_box_append(GTK_BOX(opts), tb);
out_toggles[o] = GTK_TOGGLE_BUTTON(tb);
}
return card;
}
/* A hidden green label that shows the model German sentence after Check. */ /* A hidden green label that shows the model German sentence after Check. */
GtkWidget *model_answer_add(GtkWidget *body, const char *german) { GtkWidget *model_answer_add(GtkWidget *body, const char *german) {
GtkWidget *lbl = gtk_label_new(german); GtkWidget *lbl = gtk_label_new(german);

View file

@ -88,33 +88,11 @@ GtkWidget *build_choice(UnitCtx *unit, const char *title,
ctx->trans = g_new0(GtkWidget *, n); ctx->trans = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
GtkWidget *prompt = gtk_label_new(qs[i].prompt); GtkWidget *card = mcq_append_question(body, i + 1, &qs[i],
GtkWidget *row; &ctx->toggles[i * n_opts]);
GtkToggleButton *first = NULL;
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
gtk_widget_add_css_class(prompt, "ex-prompt");
gtk_box_append(GTK_BOX(body), prompt);
row = gtk_flow_box_new();
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), row);
for (int o = 0; o < qs[i].n_options; o++) {
GtkWidget *tb = gtk_toggle_button_new_with_label(qs[i].options[o]);
gtk_widget_add_css_class(tb, "pill");
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first);
if (!first)
first = GTK_TOGGLE_BUTTON(tb);
gtk_flow_box_append(GTK_FLOW_BOX(row), tb);
ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb);
}
if (meanings[i]) if (meanings[i])
ctx->trans[i] = meaning_add(body, meanings[i]); ctx->trans[i] = meaning_add(card, meanings[i]);
} }
g_signal_connect(check, "clicked", G_CALLBACK(choice_check), ctx); g_signal_connect(check, "clicked", G_CALLBACK(choice_check), ctx);

51
hw.c
View file

@ -653,22 +653,27 @@ static GtkWidget *build_hw_mcq_page(int lesson_id, const char *back_page,
gtk_widget_set_margin_top(scroll, 12); gtk_widget_set_margin_top(scroll, 12);
gtk_box_append(GTK_BOX(page), scroll); gtk_box_append(GTK_BOX(page), scroll);
body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
gtk_widget_set_halign(body, GTK_ALIGN_FILL);
gtk_widget_set_hexpand(body, TRUE);
gtk_widget_set_margin_start(body, 4);
gtk_widget_set_margin_end(body, 4);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
head = gtk_label_new(NULL); head = gtk_label_new(NULL);
i18n_bind(head, heading_key, 0); i18n_bind(head, heading_key, 0);
gtk_widget_set_halign(head, GTK_ALIGN_START); gtk_widget_set_halign(head, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(head), TRUE); gtk_label_set_wrap(GTK_LABEL(head), TRUE);
gtk_widget_set_margin_top(head, 6); gtk_widget_set_margin_top(head, 4);
gtk_widget_add_css_class(head, "ex-sub"); gtk_widget_add_css_class(head, "quiz-heading");
gtk_box_append(GTK_BOX(body), head); gtk_box_append(GTK_BOX(body), head);
intro = gtk_label_new(NULL); intro = gtk_label_new(NULL);
i18n_bind(intro, "net_quiz_intro", 0); i18n_bind(intro, "net_quiz_intro", 0);
gtk_widget_set_halign(intro, GTK_ALIGN_START); gtk_widget_set_halign(intro, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(intro), TRUE); gtk_label_set_wrap(GTK_LABEL(intro), TRUE);
gtk_widget_add_css_class(intro, "ex-prompt"); gtk_widget_add_css_class(intro, "quiz-intro");
gtk_widget_set_margin_bottom(intro, 4);
gtk_box_append(GTK_BOX(body), intro); gtk_box_append(GTK_BOX(body), intro);
ctx->qs = qs; ctx->qs = qs;
@ -679,46 +684,20 @@ static GtkWidget *build_hw_mcq_page(int lesson_id, const char *back_page,
ctx->hints = g_new0(GtkWidget *, n); ctx->hints = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
GtkWidget *prompt; GtkWidget *card = mcq_append_question(body, i + 1, &qs[i],
GtkWidget *row; &ctx->toggles[i * n_opts]);
GtkToggleButton *first = NULL;
char *qtext;
qtext = g_strdup_printf("%d.) %s", i + 1, qs[i].prompt); ctx->hints[i] = meaning_add(card, hints[i]);
prompt = gtk_label_new(qtext);
g_free(qtext);
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
gtk_widget_add_css_class(prompt, "ex-prompt");
gtk_widget_set_margin_top(prompt, 8);
gtk_box_append(GTK_BOX(body), prompt);
row = gtk_flow_box_new();
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), row);
for (int o = 0; o < n_opts; o++) {
GtkWidget *tb = gtk_toggle_button_new_with_label(qs[i].options[o]);
gtk_widget_add_css_class(tb, "pill");
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first);
if (!first)
first = GTK_TOGGLE_BUTTON(tb);
gtk_flow_box_append(GTK_FLOW_BOX(row), tb);
ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb);
}
ctx->hints[i] = meaning_add(body, hints[i]);
} }
btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_top(btns, 12); gtk_widget_set_margin_top(btns, 8);
gtk_widget_set_margin_bottom(btns, 8);
gtk_box_append(GTK_BOX(body), btns); gtk_box_append(GTK_BOX(body), btns);
check = gtk_button_new(); check = gtk_button_new();
i18n_bind(check, "check", 1); i18n_bind(check, "check", 1);
gtk_widget_add_css_class(check, "pill"); gtk_widget_add_css_class(check, "btn-primary");
gtk_widget_set_halign(check, GTK_ALIGN_START); gtk_widget_set_halign(check, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(btns), check); gtk_box_append(GTK_BOX(btns), check);
g_signal_connect(check, "clicked", G_CALLBACK(hw_mcq_check), ctx); g_signal_connect(check, "clicked", G_CALLBACK(hw_mcq_check), ctx);

View file

@ -882,6 +882,10 @@ GtkWidget *ex_page_shell(const char *back_target, const char *title,
GtkWidget **body_out, GtkWidget **feedback_out, GtkWidget **body_out, GtkWidget **feedback_out,
GtkWidget **btn_out); GtkWidget **btn_out);
GtkWidget *meaning_add(GtkWidget *body, const char *text); GtkWidget *meaning_add(GtkWidget *body, const char *text);
GtkWidget *mcq_option_toggle(const char *text, int index,
GtkToggleButton **group_first);
GtkWidget *mcq_append_question(GtkWidget *parent, int num, const ChoiceQ *q,
GtkToggleButton **out_toggles);
GtkWidget *model_answer_add(GtkWidget *body, const char *german); GtkWidget *model_answer_add(GtkWidget *body, const char *german);
void meaning_reveal_all(GtkWidget **labels, int n); void meaning_reveal_all(GtkWidget **labels, int n);
ComboListCtx *combo_list_ctx_new(UnitCtx *unit, int ex_num, ComboListCtx *combo_list_ctx_new(UnitCtx *unit, int ex_num,

53
net.c
View file

@ -2872,22 +2872,27 @@ GtkWidget *build_net_mcq_page(int lesson_id, const char *back_page,
gtk_widget_set_margin_top(scroll, 12); gtk_widget_set_margin_top(scroll, 12);
gtk_box_append(GTK_BOX(page), scroll); gtk_box_append(GTK_BOX(page), scroll);
body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
gtk_widget_set_halign(body, GTK_ALIGN_FILL);
gtk_widget_set_hexpand(body, TRUE);
gtk_widget_set_margin_start(body, 4);
gtk_widget_set_margin_end(body, 4);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body); gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
head = gtk_label_new(NULL); head = gtk_label_new(NULL);
i18n_bind(head, heading_key, 0); i18n_bind(head, heading_key, 0);
gtk_widget_set_halign(head, GTK_ALIGN_START); gtk_widget_set_halign(head, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(head), TRUE); gtk_label_set_wrap(GTK_LABEL(head), TRUE);
gtk_widget_set_margin_top(head, 6); gtk_widget_set_margin_top(head, 4);
gtk_widget_add_css_class(head, "ex-sub"); gtk_widget_add_css_class(head, "quiz-heading");
gtk_box_append(GTK_BOX(body), head); gtk_box_append(GTK_BOX(body), head);
intro = gtk_label_new(NULL); intro = gtk_label_new(NULL);
i18n_bind(intro, "net_quiz_intro", 0); i18n_bind(intro, "net_quiz_intro", 0);
gtk_widget_set_halign(intro, GTK_ALIGN_START); gtk_widget_set_halign(intro, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(intro), TRUE); gtk_label_set_wrap(GTK_LABEL(intro), TRUE);
gtk_widget_add_css_class(intro, "ex-prompt"); gtk_widget_add_css_class(intro, "quiz-intro");
gtk_widget_set_margin_bottom(intro, 4);
gtk_box_append(GTK_BOX(body), intro); gtk_box_append(GTK_BOX(body), intro);
ctx->qs = qs; ctx->qs = qs;
@ -2898,48 +2903,20 @@ GtkWidget *build_net_mcq_page(int lesson_id, const char *back_page,
ctx->hints = g_new0(GtkWidget *, n); ctx->hints = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
GtkWidget *prompt; GtkWidget *card = mcq_append_question(body, i + 1, &qs[i],
GtkWidget *row; &ctx->toggles[i * n_opts]);
GtkToggleButton *first = NULL;
char *qtext;
qtext = g_strdup_printf("%d.) %s", i + 1, qs[i].prompt); ctx->hints[i] = meaning_add(card, hints[i]);
prompt = gtk_label_new(qtext);
g_free(qtext);
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
gtk_widget_add_css_class(prompt, "ex-prompt");
gtk_widget_set_margin_top(prompt, 8);
gtk_box_append(GTK_BOX(body), prompt);
row = gtk_flow_box_new();
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), row);
for (int o = 0; o < n_opts; o++) {
GtkWidget *tb = gtk_toggle_button_new_with_label(qs[i].options[o]);
gtk_widget_add_css_class(tb, "pill");
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first);
if (!first)
first = GTK_TOGGLE_BUTTON(tb);
gtk_flow_box_append(GTK_FLOW_BOX(row), tb);
ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb);
}
ctx->hints[i] = meaning_add(body, hints[i]);
} }
btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_margin_top(btns, 12); gtk_widget_set_margin_top(btns, 8);
gtk_widget_set_margin_bottom(btns, 8);
gtk_box_append(GTK_BOX(body), btns); gtk_box_append(GTK_BOX(body), btns);
check = gtk_button_new(); check = gtk_button_new();
i18n_bind(check, "check", 1); i18n_bind(check, "check", 1);
gtk_widget_add_css_class(check, "pill"); gtk_widget_add_css_class(check, "btn-primary");
gtk_widget_set_halign(check, GTK_ALIGN_START); gtk_widget_set_halign(check, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(btns), check); gtk_box_append(GTK_BOX(btns), check);
g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx); g_signal_connect(check, "clicked", G_CALLBACK(net_mcq_check), ctx);

View file

@ -498,7 +498,8 @@ entry.answer-wrong {
border-radius: 999px; border-radius: 999px;
padding: 6px 18px; padding: 6px 18px;
font-size: 15px; font-size: 15px;
transition: border-color 150ms ease, box-shadow 150ms ease; transition: border-color 150ms ease, box-shadow 150ms ease,
background-color 150ms ease;
} }
.pill:hover { .pill:hover {
background-image: none; background-image: none;
@ -517,19 +518,61 @@ entry.answer-wrong {
border-color: @accent; border-color: @accent;
font-weight: 700; font-weight: 700;
} }
.pill.ok { .pill.ok,
.pill.ok:checked {
background-image: linear-gradient(135deg, @success 0%, @success2 100%);
color: @on_accent;
border-color: @success; border-color: @success;
box-shadow: 0 0 0 2px alpha(@success, 0.4); box-shadow: 0 0 0 2px alpha(@success, 0.35);
} }
.pill.wrong { .pill.wrong,
.pill.wrong:checked {
background-image: linear-gradient(135deg, @error 0%, #c45c5c 100%);
color: @on_accent;
border-color: @error; border-color: @error;
box-shadow: 0 0 0 2px alpha(@error, 0.4); box-shadow: 0 0 0 2px alpha(@error, 0.35);
} }
.pill.hm-key { .pill.hm-key {
padding: 4px 5px; padding: 4px 5px;
min-width: 32px; min-width: 32px;
font-size: 13px; font-size: 13px;
} }
.quiz-card {
background-color: alpha(@bg_surface0, 0.85);
border: 1px solid @bg_surface1;
border-radius: 18px;
padding: 16px 18px 14px 18px;
box-shadow: 0 6px 18px alpha(@bg_crust, 0.18);
}
.quiz-heading {
color: @fg_text;
font-size: 18px;
font-weight: 700;
}
.quiz-intro {
color: @fg_subtext;
font-size: 14px;
font-style: italic;
}
.quiz-q {
color: @fg_text;
font-size: 16px;
font-weight: 700;
}
.quiz-options {
margin-top: 2px;
}
.quiz-pill {
border-radius: 14px;
padding: 12px 16px;
min-height: 44px;
}
.quiz-pill label {
font-weight: 500;
}
.quiz-pill:checked label {
font-weight: 700;
}
.chip { .chip {
background-image: none; background-image: none;
background-color: @bg_surface1; background-color: @bg_surface1;