Restyle the interface after Umímeto, Apple, and To dáš.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-17 17:17:54 +02:00
commit d679e81887
11 changed files with 373 additions and 313 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

View file

@ -6,9 +6,9 @@
* *
* Keep to the scales below when adding rules. * Keep to the scales below when adding rules.
* *
* type 12 caption | 13 small | 15 body | 17 lead | 20 26 30 headings * type 12 caption | 14 small | 16 body | 18 lead | 28 44 headings
* weight 400 body | 500 emphasis | 600 controls and labels | 700 headings * weight 400 body | 500 emphasis | 600 controls | 700 headings
* radius 8 controls | 12 rows and cards | 16 popovers | 999 circles * radius 999 pills | 20 cards | 16 popovers
* motion 120ms colour | 150ms elevation * motion 120ms colour | 150ms elevation
* *
* Fills are flat and elevation is neutral; accent colour marks state, never * Fills are flat and elevation is neutral; accent colour marks state, never
@ -34,8 +34,9 @@ headerbar.titlebar,
} }
.app-title { .app-title {
color: @fg_text; color: @fg_text;
font-size: 13px; font-size: 15px;
font-weight: 600; font-weight: 700;
letter-spacing: -0.3px;
} }
windowcontrols button:not(:hover), windowcontrols button:not(:hover),
windowcontrols button:not(:hover) image { windowcontrols button:not(:hover) image {
@ -74,28 +75,118 @@ tooltip {
.heading { .heading {
color: @fg_text; color: @fg_text;
font-size: 30px; font-size: 44px;
font-weight: 700; font-weight: 700;
letter-spacing: -0.5px; letter-spacing: -1.4px;
line-height: 1.08;
} }
.roadmap-title { .roadmap-title {
color: @fg_text; color: @fg_text;
font-size: 26px; font-size: 28px;
font-weight: 700; font-weight: 700;
letter-spacing: -0.3px; letter-spacing: -0.6px;
} }
.roadmap-sub { .roadmap-sub {
color: @fg_subtext; color: @fg_subtext;
font-size: 15px; font-size: 15px;
} }
.welcome-kicker {
color: @fg_text;
font-size: 15px;
font-weight: 700;
letter-spacing: -0.2px;
}
.welcome-body { .welcome-body {
color: @fg_subtext; color: @fg_subtext;
font-size: 15px; font-size: 16px;
} }
.welcome-meta { .welcome-meta {
color: @fg_overlay; color: @fg_overlay;
font-size: 12px; font-size: 12px;
} }
.welcome-page {
padding: 48px 48px 56px 48px;
min-width: 640px;
}
.welcome-feature {
background-color: @bg_mantle;
border: 1px solid alpha(@fg_text, 0.07);
border-radius: 20px;
padding: 22px 24px 18px 24px;
}
.welcome-feature-title {
color: @fg_text;
font-size: 18px;
font-weight: 700;
letter-spacing: -0.3px;
}
.welcome-feature-body {
color: @fg_subtext;
font-size: 15px;
}
.welcome-link {
background-image: none;
background-color: alpha(@fg_text, 0.05);
color: @fg_text;
border: none;
border-radius: 999px;
padding: 8px 16px;
font-size: 13px;
font-weight: 600;
box-shadow: none;
margin-top: 6px;
}
.welcome-link:hover {
background-color: alpha(@fg_text, 0.09);
}
.welcome-badge {
min-width: 116px;
min-height: 116px;
padding: 18px 16px;
border-radius: 999px;
background-color: @bg_mantle;
border: 1px solid alpha(@fg_text, 0.08);
}
.welcome-badge-value {
color: @fg_text;
font-size: 32px;
font-weight: 700;
letter-spacing: -1px;
}
.welcome-badge-label {
color: @fg_subtext;
font-size: 12px;
font-weight: 600;
}
.subject-grid {
padding: 8px 4px 24px 4px;
}
.subject-tile {
background-image: none;
background-color: @bg_mantle;
color: @fg_text;
border: 1px solid alpha(@fg_text, 0.08);
border-radius: 20px;
padding: 22px 16px 18px 16px;
min-width: 196px;
box-shadow: none;
transition: background-color 120ms ease, border-color 120ms ease;
}
.subject-tile:hover {
background-image: none;
border-color: alpha(@fg_text, 0.22);
}
.subject-tile.open:hover {
border-color: @accent;
}
.subject-tile.locked {
opacity: 0.48;
}
.subject-name {
color: @fg_text;
font-size: 13px;
font-weight: 600;
}
/* Small all-caps-ish section markers. */ /* Small all-caps-ish section markers. */
.settings-label, .settings-label,
@ -189,7 +280,7 @@ tooltip {
background-color: mix(@bg_base, @bg_surface0, 0.55); background-color: mix(@bg_base, @bg_surface0, 0.55);
background-image: none; background-image: none;
border: 1px solid alpha(@fg_text, 0.09); border: 1px solid alpha(@fg_text, 0.09);
border-radius: 12px; border-radius: 20px;
color: @fg_text; color: @fg_text;
box-shadow: none; box-shadow: none;
} }
@ -217,7 +308,7 @@ tooltip {
.sent-group { .sent-group {
background-color: alpha(@fg_text, 0.04); background-color: alpha(@fg_text, 0.04);
border: 1px solid alpha(@fg_text, 0.09); border: 1px solid alpha(@fg_text, 0.09);
border-radius: 12px; border-radius: 20px;
padding: 12px; padding: 12px;
} }
.sent-group.ok { .sent-group.ok {
@ -255,9 +346,9 @@ popover > contents,
color: @on_accent; color: @on_accent;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
padding: 10px 22px; padding: 12px 28px;
border: none; border: none;
border-radius: 8px; border-radius: 999px;
box-shadow: none; box-shadow: none;
transition: background-color 120ms ease; transition: background-color 120ms ease;
} }
@ -309,7 +400,7 @@ popover > contents,
background-color: mix(@bg_base, @bg_surface0, 0.7); background-color: mix(@bg_base, @bg_surface0, 0.7);
color: @fg_text; color: @fg_text;
border: 1px solid alpha(@fg_text, 0.12); border: 1px solid alpha(@fg_text, 0.12);
border-radius: 8px; border-radius: 999px;
padding: 7px 14px; padding: 7px 14px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
@ -411,7 +502,7 @@ popover > contents,
font-weight: 600; font-weight: 600;
padding: 14px 16px; padding: 14px 16px;
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 12px; border-radius: 20px;
box-shadow: none; box-shadow: none;
transition: border-color 120ms ease, opacity 120ms ease; transition: border-color 120ms ease, opacity 120ms ease;
} }
@ -519,7 +610,7 @@ combobox.answer-wrong button {
background-color: @accent; background-color: @accent;
border-color: @accent; border-color: @accent;
color: @on_accent; color: @on_accent;
box-shadow: 0 0 0 4px alpha(@accent, 0.2); box-shadow: none;
} }
.unit-node.locked { .unit-node.locked {
background-color: @locked_bg; background-color: @locked_bg;
@ -543,7 +634,7 @@ combobox.answer-wrong button {
border-color: shade(@success, 1.15); border-color: shade(@success, 1.15);
} }
.unit-node.current:hover { .unit-node.current:hover {
box-shadow: 0 0 0 5px alpha(@accent, 0.3); box-shadow: none;
} }
.unit-node:focus, .unit-node:focus,
.unit-node:focus-visible, .unit-node:focus-visible,
@ -762,7 +853,7 @@ flowboxchild {
background-color: @accent; background-color: @accent;
color: @on_accent; color: @on_accent;
border: none; border: none;
border-radius: 8px; border-radius: 999px;
padding: 5px 14px; padding: 5px 14px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
@ -846,9 +937,9 @@ flowboxchild {
} }
.stats-metric-value { .stats-metric-value {
color: @fg_text; color: @fg_text;
font-size: 24px; font-size: 32px;
font-weight: 700; font-weight: 700;
letter-spacing: -0.5px; letter-spacing: -1px;
} }
.stats-metric-label { .stats-metric-label {
margin-top: 2px; margin-top: 2px;

View file

@ -10,7 +10,7 @@ GtkCssProvider *theme_provider;
ThemeId app_theme_id = THEME_CATPPUCCIN; ThemeId app_theme_id = THEME_CATPPUCCIN;
ColorMode app_color_mode = MODE_DARK; ColorMode app_color_mode = MODE_LIGHT;
UiLang app_lang = LANG_CS; UiLang app_lang = LANG_CS;

View file

@ -49,13 +49,25 @@ const TrEntry tr_ui[] = {
"Could not prepare the update."}, "Could not prepare the update."},
{"update_err_devbuild", "Tento build nepochází z repozitáře (dev).", {"update_err_devbuild", "Tento build nepochází z repozitáře (dev).",
"This build did not come from the repository (dev)."}, "This build did not come from the repository (dev)."},
{"welcome_kicker", "maturita.c", "maturita.c"},
{"welcome_title", "Maturita?\nTo dáte.", "Maturita?\nYou've got this."},
{"welcome_body", {"welcome_body",
"maturita.C je vzdělávací program pro studenty středních škol a " "Procvičování k maturitě v jednom okně. Česky i anglicky, "
"gymnázií na přípravu k maturitě.\n\n" "napsané v C studenty ze SSŠVT.",
"Program je napsaný v Céčku studentama ze SSŠVT!", "Exam practice in one window. Czech and English, written in C "
"maturita.C is an educational app for high-school and gymnasium " "by students from SSŠVT."},
"students to prepare for their maturita exam.\n\n" {"welcome_feat1_title", "Efektivní procvičování", "Focused practice"},
"The program is written in C by students from SSŠVT!"}, {"welcome_feat1_body",
"Cvičení, nápovědy a vokabeltraining. Aplikace si pamatuje, kde jste skončili.",
"Exercises, hints and vocabulary drills. The app remembers where you left off."},
{"welcome_feat1_link", "Začít procvičovat", "Start practicing"},
{"welcome_feat2_title", "Čtyři otevřené předměty", "Four open subjects"},
{"welcome_feat2_body",
"Deutsch, sítě, hardware a čeština. Ostatní předměty čekají na svůj obsah.",
"German, networks, hardware and Czech. The other subjects are still locked."},
{"welcome_feat2_link", "Jaké předměty jsou otevřené?", "Which subjects are open?"},
{"welcome_stat_value", "4", "4"},
{"welcome_stat_label", "otevřené\npředměty", "open\nsubjects"},
{"continue", "Pokračuj", "Continue"}, {"continue", "Pokračuj", "Continue"},
{"roadmap_title", "Učební plán", "Learning path"}, {"roadmap_title", "Učební plán", "Learning path"},
{"roadmap_sub", "Vyberte jednotku na cestě a začněte procvičovat.", {"roadmap_sub", "Vyberte jednotku na cestě a začněte procvičovat.",
@ -210,6 +222,8 @@ const TrEntry tr_ui[] = {
{"stats_pct_fmt", "%d %%", "%d %%"}, {"stats_pct_fmt", "%d %%", "%d %%"},
{"stats_units_fmt", "%d / %d", "%d / %d"}, {"stats_units_fmt", "%d / %d", "%d / %d"},
{"subjects_title", "Předměty", "Subjects"}, {"subjects_title", "Předměty", "Subjects"},
{"subjects_sub", "Otevřený předmět spustí učební cestu. Zamčené zatím jen čekají.",
"An open subject opens its path. Locked ones are placeholders for now."},
{"Český jazyk a literatura", "Český jazyk a literatura", {"Český jazyk a literatura", "Český jazyk a literatura",
"Czech Language and Literature"}, "Czech Language and Literature"},
{"Občanská nauka", "Občanská nauka", "Civics"}, {"Občanská nauka", "Občanská nauka", "Civics"},
@ -1486,9 +1500,7 @@ void i18n_bind(GtkWidget *widget, const char *key, int kind) {
void refresh_welcome_heading(void) { void refresh_welcome_heading(void) {
if (!welcome_heading) if (!welcome_heading)
return; return;
gtk_label_set_text(GTK_LABEL(welcome_heading), gtk_label_set_text(GTK_LABEL(welcome_heading), tr("welcome_title"));
app_lang == LANG_EN ? "Welcome to maturita.C"
: "Vítejte v maturita.C");
} }
void refresh_stats_ui(void); void refresh_stats_ui(void);

View file

@ -456,7 +456,7 @@ void activate(GtkApplication *app, gpointer user_data) {
window = GTK_WINDOW(gtk_application_window_new(app)); window = GTK_WINDOW(gtk_application_window_new(app));
main_window = window; main_window = window;
gtk_window_set_title(window, "maturita.c"); gtk_window_set_title(window, "maturita.c");
gtk_window_set_default_size(window, 760, 560); gtk_window_set_default_size(window, 1080, 720);
setup_window_icon(window); setup_window_icon(window);
headerbar = gtk_header_bar_new(); headerbar = gtk_header_bar_new();

View file

@ -919,16 +919,12 @@ void draw_rail(GtkDrawingArea *area, cairo_t *cr,
void add_path_node(GtkFixed *fixed, int index); void add_path_node(GtkFixed *fixed, int index);
GtkWidget *build_roadmap_page(void); GtkWidget *build_roadmap_page(void);
void sub_layout_geometry(double avail); void sub_layout_geometry(double avail);
void sub_point(double t, double *ox, double *oy);
void sub_path(cairo_t *cr, double t0, double t1);
void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr, void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer user_data); int width, int height, gpointer user_data);
void sub_apply_layout(void); void sub_apply_layout(void);
void sub_relayout(void); void sub_relayout(void);
gboolean sub_relayout_idle(gpointer data); gboolean sub_relayout_idle(gpointer data);
void sub_relayout_later(void); void sub_relayout_later(void);
void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
gpointer data);
void draw_german_flag(GtkDrawingArea *area, cairo_t *cr, void draw_german_flag(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer data); int width, int height, gpointer data);
GtkWidget *build_subjects_page(void); GtkWidget *build_subjects_page(void);

View file

@ -38,13 +38,14 @@ GtkWidget *make_back_button(const char *target) {
GtkWidget *top_bar(const char *back_target, const char *title, GtkWidget *top_bar(const char *back_target, const char *title,
const char *subtitle) { const char *subtitle) {
GtkWidget *top = gtk_center_box_new(); GtkWidget *top = gtk_center_box_new();
gtk_widget_set_margin_top(top, 2); gtk_widget_add_css_class(top, "page-top");
gtk_widget_set_margin_bottom(top, 6); gtk_widget_set_margin_top(top, 8);
gtk_widget_set_margin_bottom(top, 18);
GtkWidget *back = make_back_button(back_target); GtkWidget *back = make_back_button(back_target);
gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back); gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
GtkWidget *center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4); GtkWidget *center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_widget_set_valign(center, GTK_ALIGN_CENTER); gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center); gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
@ -60,7 +61,7 @@ GtkWidget *top_bar(const char *back_target, const char *title,
gtk_widget_set_halign(sub, GTK_ALIGN_CENTER); gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(sub, "roadmap-sub"); gtk_widget_add_css_class(sub, "roadmap-sub");
gtk_label_set_wrap(GTK_LABEL(sub), TRUE); gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(sub), 60); gtk_label_set_max_width_chars(GTK_LABEL(sub), 56);
gtk_box_append(GTK_BOX(center), sub); gtk_box_append(GTK_BOX(center), sub);
} }
@ -93,48 +94,151 @@ gboolean on_window_key_pressed(GtkEventControllerKey *controller,
/* Welcome page */ /* Welcome page */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
static GtkWidget *welcome_feature(const char *title_key, const char *body_key,
const char *link_key) {
GtkWidget *card;
GtkWidget *title;
GtkWidget *body;
GtkWidget *link;
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_widget_add_css_class(card, "welcome-feature");
gtk_widget_set_hexpand(card, TRUE);
title = gtk_label_new(NULL);
i18n_bind(title, title_key, 0);
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_add_css_class(title, "welcome-feature-title");
gtk_box_append(GTK_BOX(card), title);
body = gtk_label_new(NULL);
i18n_bind(body, body_key, 0);
gtk_widget_set_halign(body, GTK_ALIGN_START);
gtk_label_set_wrap(GTK_LABEL(body), TRUE);
gtk_label_set_xalign(GTK_LABEL(body), 0.0);
gtk_label_set_max_width_chars(GTK_LABEL(body), 52);
gtk_widget_add_css_class(body, "welcome-feature-body");
gtk_box_append(GTK_BOX(card), body);
link = gtk_button_new();
i18n_bind(link, link_key, 1);
gtk_widget_set_halign(link, GTK_ALIGN_START);
gtk_widget_add_css_class(link, "welcome-link");
gtk_box_append(GTK_BOX(card), link);
g_signal_connect(link, "clicked", G_CALLBACK(on_continue_clicked), NULL);
return card;
}
static GtkWidget *welcome_stat(void) {
GtkWidget *badge;
GtkWidget *value;
GtkWidget *label;
badge = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_add_css_class(badge, "welcome-badge");
gtk_widget_set_valign(badge, GTK_ALIGN_CENTER);
gtk_widget_set_halign(badge, GTK_ALIGN_CENTER);
value = gtk_label_new(NULL);
i18n_bind(value, "welcome_stat_value", 0);
gtk_widget_add_css_class(value, "welcome-badge-value");
gtk_box_append(GTK_BOX(badge), value);
label = gtk_label_new(NULL);
i18n_bind(label, "welcome_stat_label", 0);
gtk_label_set_justify(GTK_LABEL(label), GTK_JUSTIFY_CENTER);
gtk_widget_add_css_class(label, "welcome-badge-label");
gtk_box_append(GTK_BOX(badge), label);
return badge;
}
GtkWidget *build_welcome_page(void) { GtkWidget *build_welcome_page(void) {
GtkWidget *box; GtkWidget *scroll;
GtkWidget *icon; GtkWidget *page;
GtkWidget *hero;
GtkWidget *copy;
GtkWidget *kicker;
GtkWidget *heading; GtkWidget *heading;
GtkWidget *body; GtkWidget *body;
GtkWidget *actions;
GtkWidget *button; GtkWidget *button;
GtkWidget *features;
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); scroll = gtk_scrolled_window_new();
gtk_widget_set_halign(box, GTK_ALIGN_CENTER); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
gtk_widget_set_valign(box, GTK_ALIGN_CENTER); GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_margin_top(box, 40); gtk_widget_set_hexpand(scroll, TRUE);
gtk_widget_set_margin_bottom(box, 40); gtk_widget_set_vexpand(scroll, TRUE);
icon = gtk_image_new_from_icon_name(ICON_NAME); page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_image_set_pixel_size(GTK_IMAGE(icon), 88); gtk_widget_add_css_class(page, "welcome-page");
gtk_widget_set_halign(icon, GTK_ALIGN_CENTER); gtk_widget_set_halign(page, GTK_ALIGN_CENTER);
gtk_widget_set_margin_bottom(icon, 22); gtk_widget_set_hexpand(page, TRUE);
gtk_box_append(GTK_BOX(box), icon); gtk_widget_set_size_request(page, 760, -1);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), page);
hero = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 40);
gtk_widget_add_css_class(hero, "welcome-hero");
gtk_widget_set_hexpand(hero, TRUE);
gtk_box_append(GTK_BOX(page), hero);
copy = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_hexpand(copy, TRUE);
gtk_box_append(GTK_BOX(hero), copy);
kicker = gtk_label_new(NULL);
i18n_bind(kicker, "welcome_kicker", 0);
gtk_widget_set_halign(kicker, GTK_ALIGN_START);
gtk_widget_add_css_class(kicker, "welcome-kicker");
gtk_box_append(GTK_BOX(copy), kicker);
heading = gtk_label_new(NULL); heading = gtk_label_new(NULL);
welcome_heading = heading; welcome_heading = heading;
gtk_widget_set_halign(heading, GTK_ALIGN_CENTER); i18n_bind(heading, "welcome_title", 0);
gtk_widget_set_halign(heading, GTK_ALIGN_START);
gtk_label_set_justify(GTK_LABEL(heading), GTK_JUSTIFY_LEFT);
gtk_label_set_xalign(GTK_LABEL(heading), 0.0);
gtk_label_set_wrap(GTK_LABEL(heading), TRUE);
gtk_widget_add_css_class(heading, "heading"); gtk_widget_add_css_class(heading, "heading");
gtk_box_append(GTK_BOX(box), heading); gtk_widget_set_margin_top(heading, 10);
gtk_box_append(GTK_BOX(copy), heading);
body = gtk_label_new(NULL); body = gtk_label_new(NULL);
i18n_bind(body, "welcome_body", 0); i18n_bind(body, "welcome_body", 0);
gtk_label_set_justify(GTK_LABEL(body), GTK_JUSTIFY_CENTER);
gtk_label_set_wrap(GTK_LABEL(body), TRUE); gtk_label_set_wrap(GTK_LABEL(body), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(body), 46); gtk_label_set_xalign(GTK_LABEL(body), 0.0);
gtk_widget_set_margin_top(body, 10); gtk_label_set_max_width_chars(GTK_LABEL(body), 42);
gtk_widget_set_halign(body, GTK_ALIGN_START);
gtk_widget_set_margin_top(body, 14);
gtk_widget_add_css_class(body, "welcome-body"); gtk_widget_add_css_class(body, "welcome-body");
gtk_box_append(GTK_BOX(box), body); gtk_box_append(GTK_BOX(copy), body);
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
gtk_widget_set_margin_top(actions, 28);
gtk_box_append(GTK_BOX(copy), actions);
button = gtk_button_new(); button = gtk_button_new();
i18n_bind(button, "continue", 1); i18n_bind(button, "continue", 1);
gtk_widget_set_halign(button, GTK_ALIGN_CENTER); gtk_widget_set_halign(button, GTK_ALIGN_START);
gtk_widget_set_margin_top(button, 28);
gtk_widget_add_css_class(button, "btn-primary"); gtk_widget_add_css_class(button, "btn-primary");
gtk_box_append(GTK_BOX(box), button); gtk_box_append(GTK_BOX(actions), button);
g_signal_connect(button, "clicked", G_CALLBACK(on_continue_clicked), NULL); g_signal_connect(button, "clicked", G_CALLBACK(on_continue_clicked), NULL);
return box; gtk_box_append(GTK_BOX(hero), welcome_stat());
features = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
gtk_widget_set_margin_top(features, 36);
gtk_box_append(GTK_BOX(page), features);
gtk_box_append(GTK_BOX(features),
welcome_feature("welcome_feat1_title",
"welcome_feat1_body",
"welcome_feat1_link"));
gtk_box_append(GTK_BOX(features),
welcome_feature("welcome_feat2_title",
"welcome_feat2_body",
"welcome_feat2_link"));
return scroll;
} }

View file

@ -1,10 +1,9 @@
#include "maturita.h" #include "maturita.h"
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Subjects page ("Předměty") - serpentine subject map */ /* Subjects page — icon grid, one tile per subject */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware) and CZ_SUBJ /* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware) and CZ_SUBJ
* (Czech) are unlocked. */ * (Czech) are unlocked. */
const char *sub_keys[NUM_SUBJECTS] = { const char *sub_keys[NUM_SUBJECTS] = {
@ -42,164 +41,36 @@ double sub_cx[NUM_SUBJECTS];
double sub_cy[NUM_SUBJECTS]; double sub_cy[NUM_SUBJECTS];
int sub_rows = 1; int sub_rows = 1;
int sub_cols = NUM_SUBJECTS; int sub_cols = NUM_SUBJECTS;
int sub_cw = (int)(2.0 * SUB_MX + (NUM_SUBJECTS - 1) * SUB_SPAC); int sub_cw;
int sub_ch = (int)(2.0 * SUB_MY); int sub_ch;
guint sub_idle; guint sub_idle;
void sub_layout_geometry(double avail) { void sub_layout_geometry(double avail) {
const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_SUBJECTS - 1); (void)avail;
int rows;
int r, c, i;
for (rows = 1; rows <= NUM_SUBJECTS; rows++) {
int cols = (NUM_SUBJECTS + rows - 1) / rows;
if (2.0 * SUB_MX + (cols - 1) * SUB_SPAC <= avail + 1.0)
break;
}
if (rows > NUM_SUBJECTS)
rows = NUM_SUBJECTS;
sub_rows = rows;
sub_cols = (NUM_SUBJECTS + rows - 1) / rows;
if (sub_cols < 1)
sub_cols = 1;
sub_cw = (int)(2.0 * SUB_MX + (sub_cols - 1) * SUB_SPAC);
sub_ch = (int)(2.0 * SUB_MY + (rows - 1) * SUB_GAP);
for (r = 0; r < rows; r++) {
int base = r * sub_cols;
int len = MIN(sub_cols, NUM_SUBJECTS - base);
int fwd = (r % 2) == 0;
for (c = 0; c < len; c++) {
i = base + c;
int cc = fwd ? c : (sub_cols - 1 - c);
sub_cx[i] = SUB_MX + cc * SUB_SPAC;
sub_cy[i] = SUB_MY + r * SUB_GAP
+ SUB_WAVE * sin((double)i * phase);
}
}
}
void sub_point(double t, double *ox, double *oy) {
int k = (int)t;
double u = t - (double)k;
double u2, u3;
double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
if (k < 0) { k = 0; u = 0.0; }
if (k >= NUM_SUBJECTS - 1) { k = NUM_SUBJECTS - 2; u = 1.0; }
p1x = sub_cx[k]; p1y = sub_cy[k];
p2x = sub_cx[k + 1]; p2y = sub_cy[k + 1];
if (k - 1 >= 0) { p0x = sub_cx[k - 1]; p0y = sub_cy[k - 1]; }
else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
if (k + 2 < NUM_SUBJECTS) { p3x = sub_cx[k + 2]; p3y = sub_cy[k + 2]; }
else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
u2 = u * u;
u3 = u2 * u;
*ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
+ (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
+ (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
*oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
+ (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
+ (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
}
void sub_path(cairo_t *cr, double t0, double t1) {
const double steps_per_unit = 48.0;
int n = (int)((t1 - t0) * steps_per_unit);
double x, y;
int s;
if (n < 1)
n = 1;
sub_point(t0, &x, &y);
cairo_move_to(cr, x, y);
for (s = 1; s <= n; s++) {
sub_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
cairo_line_to(cr, x, y);
}
} }
void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr, void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr,
int width, int height, gpointer user_data) { int width, int height, gpointer user_data) {
const double t_end = (double)(NUM_SUBJECTS - 1);
const Rgb rail = color_from_hex(app_theme.rail);
(void)area; (void)area;
(void)cr;
(void)width; (void)width;
(void)height; (void)height;
(void)user_data; (void)user_data;
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
cairo_paint(cr);
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
cairo_new_path(cr);
sub_path(cr, 0.0, t_end);
cairo_set_line_width(cr, 12);
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
cairo_stroke(cr);
} }
void sub_apply_layout(void) { void sub_apply_layout(void) {
int i;
if (!sub_fixed)
return;
for (i = 0; i < NUM_SUBJECTS; i++) {
if (sub_cells[i])
gtk_fixed_move(GTK_FIXED(sub_fixed), sub_cells[i],
(int)(sub_cx[i] - SUB_BUBBLE / 2.0),
(int)(sub_cy[i] - SUB_BUBBLE / 2.0));
if (sub_labels[i])
gtk_fixed_move(GTK_FIXED(sub_fixed), sub_labels[i],
(int)(sub_cx[i] - (SUB_SPAC - 24.0) / 2.0),
(int)(sub_cy[i] + SUB_BUBBLE / 2.0 + 10.0));
}
gtk_widget_set_size_request(sub_fixed, sub_cw, sub_ch);
gtk_widget_set_size_request(sub_rail, sub_cw, sub_ch);
gtk_fixed_move(GTK_FIXED(sub_fixed), sub_rail, 0, 0);
gtk_widget_queue_draw(sub_rail);
} }
void sub_relayout(void) { void sub_relayout(void) {
GtkAdjustment *hadj;
double avail;
if (!sub_scroll)
return;
hadj = gtk_scrolled_window_get_hadjustment(
GTK_SCROLLED_WINDOW(sub_scroll));
avail = gtk_adjustment_get_page_size(hadj);
if (avail < 1.0)
return;
sub_layout_geometry(avail);
sub_apply_layout();
} }
gboolean sub_relayout_idle(gpointer data) { gboolean sub_relayout_idle(gpointer data) {
(void)data; (void)data;
sub_idle = 0; sub_idle = 0;
sub_relayout();
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
void sub_relayout_later(void) { void sub_relayout_later(void) {
if (sub_idle == 0)
sub_idle = g_idle_add(sub_relayout_idle, NULL);
}
void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
gpointer data) {
(void)adj;
(void)ps;
(void)data;
sub_relayout_later();
} }
/* German flag (black / red / gold) drawn in a clipped circle. */ /* German flag (black / red / gold) drawn in a clipped circle. */
@ -281,144 +152,130 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
cairo_restore(cr); cairo_restore(cr);
} }
static gboolean subject_is_open(int i) {
return i == 0 || i == NET_SUBJ || i == HW_SUBJ || i == CZ_SUBJ;
}
static GtkWidget *subject_icon(int i) {
GtkWidget *area = gtk_drawing_area_new();
gtk_widget_set_size_request(area, 48, 48);
gtk_widget_set_halign(area, GTK_ALIGN_CENTER);
if (i == 0)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_german_flag, NULL, NULL);
else if (i == NET_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_wifi_icon, NULL, NULL);
else if (i == HW_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_chip_icon, NULL, NULL);
else if (i == CZ_SUBJ)
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(area),
draw_czech_flag, NULL, NULL);
else
return icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 22);
return area;
}
static const char *subject_target(int i) {
if (i == 0)
return "roadmap";
if (i == NET_SUBJ)
return "netyears";
if (i == HW_SUBJ)
return "hwmap";
if (i == CZ_SUBJ)
return "czechmap";
return NULL;
}
GtkWidget *build_subjects_page(void) { GtkWidget *build_subjects_page(void) {
GtkWidget *page; GtkWidget *page;
GtkWidget *scroll; GtkWidget *scroll;
GtkWidget *wrap; GtkWidget *grid;
GtkWidget *fixed;
GtkWidget *rail;
GtkAdjustment *ha;
GtkAdjustment *va;
int i; int i;
sub_layout_geometry(1000.0);
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_add_css_class(page, "subjects-page");
gtk_widget_set_hexpand(page, TRUE); gtk_widget_set_hexpand(page, TRUE);
gtk_widget_set_vexpand(page, TRUE); gtk_widget_set_vexpand(page, TRUE);
gtk_widget_set_margin_start(page, 32); gtk_widget_set_margin_start(page, 40);
gtk_widget_set_margin_end(page, 32); gtk_widget_set_margin_end(page, 40);
gtk_widget_set_margin_top(page, 24); gtk_widget_set_margin_top(page, 16);
gtk_widget_set_margin_bottom(page, 24); gtk_widget_set_margin_bottom(page, 28);
gtk_box_append(GTK_BOX(page), gtk_box_append(GTK_BOX(page),
top_bar("welcome", "subjects_title", NULL)); top_bar("welcome", "subjects_title", "subjects_sub"));
scroll = gtk_scrolled_window_new(); scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE); gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 14); gtk_widget_set_margin_top(scroll, 8);
gtk_box_append(GTK_BOX(page), scroll); gtk_box_append(GTK_BOX(page), scroll);
sub_scroll = scroll; sub_scroll = scroll;
wrap = gtk_center_box_new(); grid = gtk_flow_box_new();
gtk_widget_set_vexpand(wrap, TRUE); gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(grid), 5);
gtk_widget_set_hexpand(wrap, TRUE); gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(grid), 3);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap); gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(grid), GTK_SELECTION_NONE);
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(grid), TRUE);
fixed = gtk_fixed_new(); gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(grid), 14);
gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER); gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(grid), 14);
gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER); gtk_widget_set_halign(grid, GTK_ALIGN_CENTER);
gtk_widget_set_size_request(fixed, sub_cw, sub_ch); gtk_widget_set_hexpand(grid, TRUE);
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); gtk_widget_add_css_class(grid, "subject-grid");
sub_fixed = fixed; gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), grid);
sub_fixed = grid;
rail = gtk_drawing_area_new();
gtk_widget_set_size_request(rail, sub_cw, sub_ch);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_sub_rail,
NULL, NULL);
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
sub_rail = rail;
for (i = 0; i < NUM_SUBJECTS; i++) { for (i = 0; i < NUM_SUBJECTS; i++) {
GtkWidget *card; GtkWidget *card;
GtkWidget *inner;
GtkWidget *icon;
GtkWidget *lbl; GtkWidget *lbl;
const char *target;
gboolean open = subject_is_open(i);
card = gtk_button_new(); card = gtk_button_new();
gtk_widget_add_css_class(card, "unit-node"); gtk_widget_add_css_class(card, "subject-tile");
gtk_widget_set_can_focus(card, FALSE); gtk_widget_set_can_focus(card, open);
gtk_widget_set_size_request(card, (int)SUB_BUBBLE, (int)SUB_BUBBLE); gtk_widget_set_hexpand(card, TRUE);
gtk_widget_set_size_request(card, 180, 132);
if (i == 0) { inner = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
GtkWidget *flag = gtk_drawing_area_new(); gtk_widget_set_halign(inner, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(card, "current"); gtk_widget_set_valign(inner, GTK_ALIGN_CENTER);
gtk_widget_set_size_request(flag, (int)SUB_BUBBLE, gtk_button_set_child(GTK_BUTTON(card), inner);
(int)SUB_BUBBLE);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(flag),
draw_german_flag, NULL, NULL);
gtk_button_set_child(GTK_BUTTON(card), flag);
g_object_set_data_full(G_OBJECT(card), "target",
g_strdup("roadmap"), g_free);
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} else if (i == NET_SUBJ) {
GtkWidget *wifi = gtk_drawing_area_new();
gtk_widget_add_css_class(card, "current");
gtk_widget_set_size_request(wifi, (int)SUB_BUBBLE,
(int)SUB_BUBBLE);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(wifi),
draw_wifi_icon, NULL, NULL);
gtk_button_set_child(GTK_BUTTON(card), wifi);
g_object_set_data_full(G_OBJECT(card), "target",
g_strdup("netyears"), g_free);
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} else if (i == HW_SUBJ) {
GtkWidget *chip = gtk_drawing_area_new();
gtk_widget_add_css_class(card, "current");
gtk_widget_set_size_request(chip, (int)SUB_BUBBLE,
(int)SUB_BUBBLE);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(chip),
draw_chip_icon, NULL, NULL);
gtk_button_set_child(GTK_BUTTON(card), chip);
g_object_set_data_full(G_OBJECT(card), "target",
g_strdup("hwmap"), g_free);
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} else if (i == CZ_SUBJ) {
GtkWidget *flag = gtk_drawing_area_new();
gtk_widget_add_css_class(card, "current");
gtk_widget_set_size_request(flag, (int)SUB_BUBBLE,
(int)SUB_BUBBLE);
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(flag),
draw_czech_flag, NULL, NULL);
gtk_button_set_child(GTK_BUTTON(card), flag);
g_object_set_data_full(G_OBJECT(card), "target",
g_strdup("czechmap"), g_free);
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} else {
GtkWidget *lock;
gtk_widget_add_css_class(card, "locked");
gtk_widget_set_sensitive(card, FALSE);
lock = icon_area_new(draw_lock_icon,
0.3451, 0.3569, 0.4392, 26);
gtk_button_set_child(GTK_BUTTON(card), lock);
}
sub_cells[i] = card; icon = subject_icon(i);
gtk_fixed_put(GTK_FIXED(fixed), card, 0, 0); gtk_box_append(GTK_BOX(inner), icon);
lbl = gtk_label_new(NULL); lbl = gtk_label_new(NULL);
i18n_bind(lbl, sub_keys[i], 0); i18n_bind(lbl, sub_keys[i], 0);
gtk_widget_set_size_request(lbl, (int)(SUB_SPAC - 24.0), -1);
gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER); gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER); gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
gtk_label_set_wrap(GTK_LABEL(lbl), TRUE); gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
gtk_widget_add_css_class(lbl, "unit-name"); gtk_label_set_max_width_chars(GTK_LABEL(lbl), 16);
if (i != 0 && i != NET_SUBJ && i != HW_SUBJ && i != CZ_SUBJ) gtk_widget_add_css_class(lbl, "subject-name");
gtk_box_append(GTK_BOX(inner), lbl);
if (open) {
gtk_widget_add_css_class(card, "open");
target = subject_target(i);
g_object_set_data_full(G_OBJECT(card), "target",
g_strdup(target), g_free);
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
} else {
gtk_widget_add_css_class(card, "locked");
gtk_widget_add_css_class(lbl, "unit-name-locked"); gtk_widget_add_css_class(lbl, "unit-name-locked");
gtk_widget_set_sensitive(card, FALSE);
}
sub_cells[i] = card;
sub_labels[i] = lbl; sub_labels[i] = lbl;
gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0); gtk_flow_box_insert(GTK_FLOW_BOX(grid), card, -1);
} }
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",
G_CALLBACK(sub_adjust_notify), NULL);
g_signal_connect(va, "notify::page-size",
G_CALLBACK(sub_adjust_notify), NULL);
sub_apply_layout();
sub_relayout_later();
return page; return page;
} }

View file

@ -18,18 +18,18 @@ Rgb mix_rgb(Rgb a, Rgb b, double t) {
} }
ThemePalette theme_palette(ThemeId id, ColorMode mode) { ThemePalette theme_palette(ThemeId id, ColorMode mode) {
/* Catppuccin Mocha / Latte */ /* Paper: cream page, white cards, yellow CTA, near-black type. */
static const ThemePalette cat_dark = { static const ThemePalette cat_dark = {
0x11111b, 0x1e1e2e, 0x181825, 0x313244, 0x45475a, 0x585b70, 0x000000, 0x000000, 0x111111, 0x1d1d1f, 0x2c2c2e, 0x3a3a3c,
0x6c7086, 0xcdd6f4, 0xa6adc8, 0xcba6f7, 0xb4befe, 0x89b4fa, 0x86868b, 0xf5f5f7, 0xa1a1a6, 0xffd60a, 0x2997ff, 0x64d2ff,
0xa6e3a1, 0x94e2d5, 0xf9e2af, 0xf38ba8, 0x1e1e2e, 0x30d158, 0x64d2ff, 0xffd60a, 0xff453a, 0x1d1d1f,
0x2b2d40, 0x24263a, 0x383b52, 0x27293c, 0x1f202e, 0x3b3e58, 0x52556b 0x1d1d1f, 0x111111, 0x3a3a3c, 0x2c2c2e, 0x000000, 0x3a3a3c, 0x86868b
}; };
static const ThemePalette cat_light = { static const ThemePalette cat_light = {
0xdce0e8, 0xeff1f5, 0xe6e9ef, 0xccd0da, 0xbcc0cc, 0xacb0be, 0xf3eee6, 0xfaf7f2, 0xffffff, 0xffffff, 0xf0ebe3, 0xe5dfd4,
0x9ca0b0, 0x4c4f69, 0x6c6f85, 0x8839ef, 0x7287fd, 0x1e66f5, 0x8a8378, 0x1a1a1a, 0x5c574f, 0xffcc00, 0x0071e3, 0x2997ff,
0x40a02b, 0x179299, 0xdf8e1d, 0xd20f39, 0xeff1f5, 0x1f8a4c, 0x1a7f4a, 0xe6a800, 0xd32f2f, 0x1a1a1a,
0xe6e9ef, 0xdce0e8, 0xbcc0cc, 0xccd0da, 0xdce0e8, 0xbcc0cc, 0x9ca0b0 0xffffff, 0xf0ebe3, 0xd5cfc4, 0xe5dfd4, 0xf3eee6, 0xd5cfc4, 0x8a8378
}; };
/* Nord */ /* Nord */
static const ThemePalette nord_dark = { static const ThemePalette nord_dark = {