diff --git a/assets/germanlection.png b/assets/germanlection.png index cb18d71..bee7449 100644 Binary files a/assets/germanlection.png and b/assets/germanlection.png differ diff --git a/assets/subjectsscreen.png b/assets/subjectsscreen.png index d0ffed6..b976705 100644 Binary files a/assets/subjectsscreen.png and b/assets/subjectsscreen.png differ diff --git a/assets/welcomescreen.png b/assets/welcomescreen.png index 51a1860..4e89820 100644 Binary files a/assets/welcomescreen.png and b/assets/welcomescreen.png differ diff --git a/data/style.css b/data/style.css index fd60088..82b1353 100644 --- a/data/style.css +++ b/data/style.css @@ -6,9 +6,9 @@ * * Keep to the scales below when adding rules. * - * type 12 caption | 13 small | 15 body | 17 lead | 20 26 30 headings - * weight 400 body | 500 emphasis | 600 controls and labels | 700 headings - * radius 8 controls | 12 rows and cards | 16 popovers | 999 circles + * type 12 caption | 14 small | 16 body | 18 lead | 28 44 headings + * weight 400 body | 500 emphasis | 600 controls | 700 headings + * radius 999 pills | 20 cards | 16 popovers * motion 120ms colour | 150ms elevation * * Fills are flat and elevation is neutral; accent colour marks state, never @@ -34,8 +34,9 @@ headerbar.titlebar, } .app-title { color: @fg_text; - font-size: 13px; - font-weight: 600; + font-size: 15px; + font-weight: 700; + letter-spacing: -0.3px; } windowcontrols button:not(:hover), windowcontrols button:not(:hover) image { @@ -74,28 +75,118 @@ tooltip { .heading { color: @fg_text; - font-size: 30px; + font-size: 44px; font-weight: 700; - letter-spacing: -0.5px; + letter-spacing: -1.4px; + line-height: 1.08; } .roadmap-title { color: @fg_text; - font-size: 26px; + font-size: 28px; font-weight: 700; - letter-spacing: -0.3px; + letter-spacing: -0.6px; } .roadmap-sub { color: @fg_subtext; font-size: 15px; } +.welcome-kicker { + color: @fg_text; + font-size: 15px; + font-weight: 700; + letter-spacing: -0.2px; +} .welcome-body { color: @fg_subtext; - font-size: 15px; + font-size: 16px; } .welcome-meta { color: @fg_overlay; 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. */ .settings-label, @@ -189,7 +280,7 @@ tooltip { background-color: mix(@bg_base, @bg_surface0, 0.55); background-image: none; border: 1px solid alpha(@fg_text, 0.09); - border-radius: 12px; + border-radius: 20px; color: @fg_text; box-shadow: none; } @@ -217,7 +308,7 @@ tooltip { .sent-group { background-color: alpha(@fg_text, 0.04); border: 1px solid alpha(@fg_text, 0.09); - border-radius: 12px; + border-radius: 20px; padding: 12px; } .sent-group.ok { @@ -255,9 +346,9 @@ popover > contents, color: @on_accent; font-size: 15px; font-weight: 600; - padding: 10px 22px; + padding: 12px 28px; border: none; - border-radius: 8px; + border-radius: 999px; box-shadow: none; transition: background-color 120ms ease; } @@ -309,7 +400,7 @@ popover > contents, background-color: mix(@bg_base, @bg_surface0, 0.7); color: @fg_text; border: 1px solid alpha(@fg_text, 0.12); - border-radius: 8px; + border-radius: 999px; padding: 7px 14px; font-size: 14px; font-weight: 500; @@ -411,7 +502,7 @@ popover > contents, font-weight: 600; padding: 14px 16px; border: 2px solid transparent; - border-radius: 12px; + border-radius: 20px; box-shadow: none; transition: border-color 120ms ease, opacity 120ms ease; } @@ -519,7 +610,7 @@ combobox.answer-wrong button { background-color: @accent; border-color: @accent; color: @on_accent; - box-shadow: 0 0 0 4px alpha(@accent, 0.2); + box-shadow: none; } .unit-node.locked { background-color: @locked_bg; @@ -543,7 +634,7 @@ combobox.answer-wrong button { border-color: shade(@success, 1.15); } .unit-node.current:hover { - box-shadow: 0 0 0 5px alpha(@accent, 0.3); + box-shadow: none; } .unit-node:focus, .unit-node:focus-visible, @@ -762,7 +853,7 @@ flowboxchild { background-color: @accent; color: @on_accent; border: none; - border-radius: 8px; + border-radius: 999px; padding: 5px 14px; font-size: 13px; font-weight: 600; @@ -846,9 +937,9 @@ flowboxchild { } .stats-metric-value { color: @fg_text; - font-size: 24px; + font-size: 32px; font-weight: 700; - letter-spacing: -0.5px; + letter-spacing: -1px; } .stats-metric-label { margin-top: 2px; diff --git a/src/globals.c b/src/globals.c index 2c12b58..c93c53a 100644 --- a/src/globals.c +++ b/src/globals.c @@ -10,7 +10,7 @@ GtkCssProvider *theme_provider; ThemeId app_theme_id = THEME_CATPPUCCIN; -ColorMode app_color_mode = MODE_DARK; +ColorMode app_color_mode = MODE_LIGHT; UiLang app_lang = LANG_CS; diff --git a/src/i18n.c b/src/i18n.c index 07e45a5..1ab9a06 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -49,13 +49,25 @@ const TrEntry tr_ui[] = { "Could not prepare the update."}, {"update_err_devbuild", "Tento build nepochází z repozitáře (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", - "maturita.C je vzdělávací program pro studenty středních škol a " - "gymnázií na přípravu k maturitě.\n\n" - "Program je napsaný v Céčku studentama ze SSŠVT!", - "maturita.C is an educational app for high-school and gymnasium " - "students to prepare for their maturita exam.\n\n" - "The program is written in C by students from SSŠVT!"}, + "Procvičování k maturitě v jednom okně. Česky i anglicky, " + "napsané v C studenty ze SSŠVT.", + "Exam practice in one window. Czech and English, written in C " + "by students from SSŠVT."}, + {"welcome_feat1_title", "Efektivní procvičování", "Focused practice"}, + {"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"}, {"roadmap_title", "Učební plán", "Learning path"}, {"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_units_fmt", "%d / %d", "%d / %d"}, {"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", "Czech Language and Literature"}, {"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) { if (!welcome_heading) return; - gtk_label_set_text(GTK_LABEL(welcome_heading), - app_lang == LANG_EN ? "Welcome to maturita.C" - : "Vítejte v maturita.C"); + gtk_label_set_text(GTK_LABEL(welcome_heading), tr("welcome_title")); } void refresh_stats_ui(void); diff --git a/src/main.c b/src/main.c index 2a2a0f1..cd3d1fd 100644 --- a/src/main.c +++ b/src/main.c @@ -456,7 +456,7 @@ void activate(GtkApplication *app, gpointer user_data) { window = GTK_WINDOW(gtk_application_window_new(app)); main_window = window; 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); headerbar = gtk_header_bar_new(); diff --git a/src/maturita.h b/src/maturita.h index ba32bc9..e3a6c3c 100644 --- a/src/maturita.h +++ b/src/maturita.h @@ -919,16 +919,12 @@ void draw_rail(GtkDrawingArea *area, cairo_t *cr, void add_path_node(GtkFixed *fixed, int index); GtkWidget *build_roadmap_page(void); 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, int width, int height, gpointer user_data); void sub_apply_layout(void); void sub_relayout(void); gboolean sub_relayout_idle(gpointer data); void sub_relayout_later(void); -void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps, - gpointer data); void draw_german_flag(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data); GtkWidget *build_subjects_page(void); diff --git a/src/navigation.c b/src/navigation.c index 5641a3c..f181680 100644 --- a/src/navigation.c +++ b/src/navigation.c @@ -38,13 +38,14 @@ GtkWidget *make_back_button(const char *target) { GtkWidget *top_bar(const char *back_target, const char *title, const char *subtitle) { GtkWidget *top = gtk_center_box_new(); - gtk_widget_set_margin_top(top, 2); - gtk_widget_set_margin_bottom(top, 6); + gtk_widget_add_css_class(top, "page-top"); + gtk_widget_set_margin_top(top, 8); + gtk_widget_set_margin_bottom(top, 18); GtkWidget *back = make_back_button(back_target); 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_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_add_css_class(sub, "roadmap-sub"); 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); } @@ -93,48 +94,151 @@ gboolean on_window_key_pressed(GtkEventControllerKey *controller, /* 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 *box; - GtkWidget *icon; + GtkWidget *scroll; + GtkWidget *page; + GtkWidget *hero; + GtkWidget *copy; + GtkWidget *kicker; GtkWidget *heading; GtkWidget *body; + GtkWidget *actions; GtkWidget *button; + GtkWidget *features; - box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_widget_set_halign(box, GTK_ALIGN_CENTER); - gtk_widget_set_valign(box, GTK_ALIGN_CENTER); - gtk_widget_set_margin_top(box, 40); - gtk_widget_set_margin_bottom(box, 40); + scroll = gtk_scrolled_window_new(); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_widget_set_hexpand(scroll, TRUE); + gtk_widget_set_vexpand(scroll, TRUE); - icon = gtk_image_new_from_icon_name(ICON_NAME); - gtk_image_set_pixel_size(GTK_IMAGE(icon), 88); - gtk_widget_set_halign(icon, GTK_ALIGN_CENTER); - gtk_widget_set_margin_bottom(icon, 22); - gtk_box_append(GTK_BOX(box), icon); + page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_add_css_class(page, "welcome-page"); + gtk_widget_set_halign(page, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(page, TRUE); + 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); 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_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); 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_max_width_chars(GTK_LABEL(body), 46); - gtk_widget_set_margin_top(body, 10); + gtk_label_set_xalign(GTK_LABEL(body), 0.0); + 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_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(); i18n_bind(button, "continue", 1); - gtk_widget_set_halign(button, GTK_ALIGN_CENTER); - gtk_widget_set_margin_top(button, 28); + gtk_widget_set_halign(button, GTK_ALIGN_START); 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); - 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; } diff --git a/src/subjects.c b/src/subjects.c index 69a0037..023cfe6 100644 --- a/src/subjects.c +++ b/src/subjects.c @@ -1,10 +1,9 @@ #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 * (Czech) are unlocked. */ const char *sub_keys[NUM_SUBJECTS] = { @@ -42,164 +41,36 @@ double sub_cx[NUM_SUBJECTS]; double sub_cy[NUM_SUBJECTS]; int sub_rows = 1; int sub_cols = NUM_SUBJECTS; -int sub_cw = (int)(2.0 * SUB_MX + (NUM_SUBJECTS - 1) * SUB_SPAC); -int sub_ch = (int)(2.0 * SUB_MY); +int sub_cw; +int sub_ch; guint sub_idle; void sub_layout_geometry(double avail) { - const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_SUBJECTS - 1); - 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)avail; } void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr, 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)cr; (void)width; (void)height; (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) { - 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) { - 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) { (void)data; sub_idle = 0; - sub_relayout(); return G_SOURCE_REMOVE; } 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. */ @@ -281,144 +152,130 @@ void draw_czech_flag(GtkDrawingArea *area, cairo_t *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 *page; GtkWidget *scroll; - GtkWidget *wrap; - GtkWidget *fixed; - GtkWidget *rail; - GtkAdjustment *ha; - GtkAdjustment *va; + GtkWidget *grid; int i; - sub_layout_geometry(1000.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_vexpand(page, TRUE); - gtk_widget_set_margin_start(page, 32); - gtk_widget_set_margin_end(page, 32); - gtk_widget_set_margin_top(page, 24); - gtk_widget_set_margin_bottom(page, 24); + gtk_widget_set_margin_start(page, 40); + gtk_widget_set_margin_end(page, 40); + gtk_widget_set_margin_top(page, 16); + gtk_widget_set_margin_bottom(page, 28); gtk_box_append(GTK_BOX(page), - top_bar("welcome", "subjects_title", NULL)); + top_bar("welcome", "subjects_title", "subjects_sub")); scroll = gtk_scrolled_window_new(); 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_margin_top(scroll, 14); + gtk_widget_set_margin_top(scroll, 8); gtk_box_append(GTK_BOX(page), scroll); sub_scroll = scroll; - wrap = gtk_center_box_new(); - gtk_widget_set_vexpand(wrap, TRUE); - gtk_widget_set_hexpand(wrap, TRUE); - gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap); - - fixed = gtk_fixed_new(); - gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER); - gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER); - gtk_widget_set_size_request(fixed, sub_cw, sub_ch); - gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed); - sub_fixed = fixed; - - 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; + grid = gtk_flow_box_new(); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(grid), 5); + gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(grid), 3); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(grid), GTK_SELECTION_NONE); + gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(grid), TRUE); + gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(grid), 14); + gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(grid), 14); + gtk_widget_set_halign(grid, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(grid, TRUE); + gtk_widget_add_css_class(grid, "subject-grid"); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), grid); + sub_fixed = grid; for (i = 0; i < NUM_SUBJECTS; i++) { GtkWidget *card; + GtkWidget *inner; + GtkWidget *icon; GtkWidget *lbl; + const char *target; + gboolean open = subject_is_open(i); card = gtk_button_new(); - gtk_widget_add_css_class(card, "unit-node"); - gtk_widget_set_can_focus(card, FALSE); - gtk_widget_set_size_request(card, (int)SUB_BUBBLE, (int)SUB_BUBBLE); + gtk_widget_add_css_class(card, "subject-tile"); + gtk_widget_set_can_focus(card, open); + gtk_widget_set_hexpand(card, TRUE); + gtk_widget_set_size_request(card, 180, 132); - if (i == 0) { - 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_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); - } + inner = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); + gtk_widget_set_halign(inner, GTK_ALIGN_CENTER); + gtk_widget_set_valign(inner, GTK_ALIGN_CENTER); + gtk_button_set_child(GTK_BUTTON(card), inner); - sub_cells[i] = card; - gtk_fixed_put(GTK_FIXED(fixed), card, 0, 0); + icon = subject_icon(i); + gtk_box_append(GTK_BOX(inner), icon); lbl = gtk_label_new(NULL); 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_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER); gtk_label_set_wrap(GTK_LABEL(lbl), TRUE); - gtk_widget_add_css_class(lbl, "unit-name"); - if (i != 0 && i != NET_SUBJ && i != HW_SUBJ && i != CZ_SUBJ) + gtk_label_set_max_width_chars(GTK_LABEL(lbl), 16); + 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_set_sensitive(card, FALSE); + } + + sub_cells[i] = card; 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; } - diff --git a/src/theme.c b/src/theme.c index db9e3d3..1ed5566 100644 --- a/src/theme.c +++ b/src/theme.c @@ -18,18 +18,18 @@ Rgb mix_rgb(Rgb a, Rgb b, double t) { } 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 = { - 0x11111b, 0x1e1e2e, 0x181825, 0x313244, 0x45475a, 0x585b70, - 0x6c7086, 0xcdd6f4, 0xa6adc8, 0xcba6f7, 0xb4befe, 0x89b4fa, - 0xa6e3a1, 0x94e2d5, 0xf9e2af, 0xf38ba8, 0x1e1e2e, - 0x2b2d40, 0x24263a, 0x383b52, 0x27293c, 0x1f202e, 0x3b3e58, 0x52556b + 0x000000, 0x000000, 0x111111, 0x1d1d1f, 0x2c2c2e, 0x3a3a3c, + 0x86868b, 0xf5f5f7, 0xa1a1a6, 0xffd60a, 0x2997ff, 0x64d2ff, + 0x30d158, 0x64d2ff, 0xffd60a, 0xff453a, 0x1d1d1f, + 0x1d1d1f, 0x111111, 0x3a3a3c, 0x2c2c2e, 0x000000, 0x3a3a3c, 0x86868b }; static const ThemePalette cat_light = { - 0xdce0e8, 0xeff1f5, 0xe6e9ef, 0xccd0da, 0xbcc0cc, 0xacb0be, - 0x9ca0b0, 0x4c4f69, 0x6c6f85, 0x8839ef, 0x7287fd, 0x1e66f5, - 0x40a02b, 0x179299, 0xdf8e1d, 0xd20f39, 0xeff1f5, - 0xe6e9ef, 0xdce0e8, 0xbcc0cc, 0xccd0da, 0xdce0e8, 0xbcc0cc, 0x9ca0b0 + 0xf3eee6, 0xfaf7f2, 0xffffff, 0xffffff, 0xf0ebe3, 0xe5dfd4, + 0x8a8378, 0x1a1a1a, 0x5c574f, 0xffcc00, 0x0071e3, 0x2997ff, + 0x1f8a4c, 0x1a7f4a, 0xe6a800, 0xd32f2f, 0x1a1a1a, + 0xffffff, 0xf0ebe3, 0xd5cfc4, 0xe5dfd4, 0xf3eee6, 0xd5cfc4, 0x8a8378 }; /* Nord */ static const ThemePalette nord_dark = {