diff --git a/programs/include/gui/mtk/widgets.hpp b/programs/include/gui/mtk/widgets.hpp index 2b127e0..6899ccf 100644 --- a/programs/include/gui/mtk/widgets.hpp +++ b/programs/include/gui/mtk/widgets.hpp @@ -774,6 +774,122 @@ inline void draw_separator(Canvas& c, int x, int y, int w, const Theme& theme) { c.hline(x, y, w, theme.border); } +static constexpr int SCROLLBAR_W = 12; +static constexpr int SCROLLBAR_OUTER_PAD = 4; +static constexpr int SCROLLBAR_TRACK_PAD = 8; +static constexpr int SCROLLBAR_THUMB_INSET = 2; +static constexpr int SCROLLBAR_MIN_THUMB = 24; + +inline bool scrollbar_needed(int content_extent, int view_extent) { + return content_extent > view_extent && view_extent > 0; +} + +inline int scrollbar_max_offset(int content_extent, int view_extent) { + int max_offset = content_extent - view_extent; + return max_offset > 0 ? max_offset : 0; +} + +inline Rect scrollbar_track_rect(const Rect& viewport, + int width = SCROLLBAR_W, + int outer_pad = SCROLLBAR_OUTER_PAD, + int track_pad = SCROLLBAR_TRACK_PAD) { + int h = gui_max(viewport.h - track_pad * 2, 0); + int usable_w = viewport.w - outer_pad; + if (usable_w <= 0 || h <= 0) return {0, 0, 0, 0}; + int actual_w = gui_clamp(width, 1, usable_w); + return { + viewport.x + viewport.w - actual_w - outer_pad, + viewport.y + track_pad, + actual_w, + h + }; +} + +inline int scrollbar_thumb_extent(int content_extent, + int view_extent, + int track_extent, + int min_thumb = SCROLLBAR_MIN_THUMB) { + if (!scrollbar_needed(content_extent, view_extent) || track_extent <= 0) + return track_extent; + + int thumb = (int)(((int64_t)view_extent * track_extent) / content_extent); + return gui_clamp(thumb, gui_min(min_thumb, track_extent), track_extent); +} + +inline Rect scrollbar_thumb_rect(const Rect& track, + int content_extent, + int view_extent, + int offset, + int min_thumb = SCROLLBAR_MIN_THUMB, + int thumb_inset = SCROLLBAR_THUMB_INSET) { + if (track.empty() || !scrollbar_needed(content_extent, view_extent)) + return {0, 0, 0, 0}; + + int max_offset = scrollbar_max_offset(content_extent, view_extent); + int thumb_h = scrollbar_thumb_extent(content_extent, view_extent, + track.h, min_thumb); + int travel = gui_max(track.h - thumb_h, 0); + int clamped_offset = gui_clamp(offset, 0, max_offset); + int thumb_y = track.y + (max_offset > 0 + ? (int)(((int64_t)clamped_offset * travel) / max_offset) + : 0); + int thumb_w = gui_max(track.w - thumb_inset * 2, 1); + return {track.x + thumb_inset, thumb_y, thumb_w, thumb_h}; +} + +inline int scrollbar_offset_from_thumb_top(const Rect& track, + int content_extent, + int view_extent, + int thumb_extent, + int thumb_top) { + int max_offset = scrollbar_max_offset(content_extent, view_extent); + int travel = gui_max(track.h - thumb_extent, 0); + if (track.empty() || max_offset <= 0 || travel <= 0) + return 0; + + int rel = gui_clamp(thumb_top - track.y, 0, travel); + return (int)(((int64_t)rel * max_offset + travel / 2) / travel); +} + +inline Color scrollbar_thumb_color(const Theme& theme, + bool hovered, + bool dragging) { + Color base = mix(theme.border, theme.text_muted, 54); + if (dragging) return mix(base, theme.accent, 118); + if (hovered) return mix(base, theme.accent, 42); + return base; +} + +inline void draw_scrollbar(Canvas& c, + const Rect& track, + const Rect& thumb, + bool hovered, + bool dragging, + const Theme& theme) { + if (track.empty() || thumb.empty()) return; + + if (hovered || dragging) { + int rail_w = track.w > 6 ? track.w - 6 : track.w; + if (rail_w < 1) rail_w = 1; + Rect rail = { + track.x + (track.w - rail_w) / 2, + track.y, + rail_w, + track.h + }; + Color rail_fill = dragging + ? mix(theme.window_bg, theme.accent, 34) + : mix(theme.window_bg, theme.text_muted, 24); + c.fill_rounded_rect(rail.x, rail.y, rail.w, rail.h, + gui_max(theme.radius_sm, rail.w / 2), + rail_fill); + } + + Color thumb_fill = scrollbar_thumb_color(theme, hovered, dragging); + int radius = gui_max(theme.radius_sm, thumb.w / 2); + c.fill_rounded_rect(thumb.x, thumb.y, thumb.w, thumb.h, radius, thumb_fill); +} + inline Rect tab_rect(const Rect& bar, int count, int index) { if (count <= 0) return {bar.x, bar.y, 0, bar.h}; int slot_w = bar.w / count; diff --git a/programs/src/wikipedia/main.cpp b/programs/src/wikipedia/main.cpp index f623a21..619bb1e 100644 --- a/programs/src/wikipedia/main.cpp +++ b/programs/src/wikipedia/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ using namespace gui::colors; static constexpr int INIT_W = 820; static constexpr int INIT_H = 580; static int TOOLBAR_H = 42; -static constexpr int SCROLLBAR_W = 14; +static constexpr int SCROLLBAR_W = mtk::SCROLLBAR_W; static int FONT_SIZE = 18; static int TITLE_SIZE = 32; static int SECTION_SIZE = 24; @@ -39,7 +40,7 @@ static constexpr int RESP_MAX = 131072; static constexpr int MAX_LINES = 2000; static const char WIKI_HOST[] = "en.wikipedia.org"; -static const char DISPLAY_ICON_PATH[] = "0:/icons/dialog-text-and-font.svg"; +static const char DISPLAY_ICON_PATH[] = "0:/icons/text_line_spacing.svg"; // ============================================================================ // Display line @@ -57,7 +58,7 @@ struct WikiLine { // App state // ============================================================================ -enum class AppPhase { IDLE, LOADING, DONE, ERR }; +enum class AppPhase { WELCOME, IDLE, LOADING, DONE, ERR }; enum class ReaderTextSize : uint8_t { SMALL = 0, MEDIUM, LARGE, COUNT }; enum class ReaderFontFace : uint8_t { SANS = 0, SERIF, BOOK, COUNT }; @@ -81,7 +82,7 @@ struct ReaderPalette { static constexpr int READER_TEXT_SIZE_COUNT = (int)ReaderTextSize::COUNT; static constexpr int READER_FONT_FACE_COUNT = (int)ReaderFontFace::COUNT; -static AppPhase g_phase = AppPhase::IDLE; +static AppPhase g_phase = AppPhase::WELCOME; static char g_query[256] = {}; static mtk::TextInputState g_query_input = {}; static char g_status[256] = {}; @@ -97,12 +98,18 @@ static bool g_scroll_dragging = false; static int g_scroll_drag_offset = 0; static bool g_display_controls_open = false; static ReaderTextSize g_reader_text_size = ReaderTextSize::MEDIUM; -static ReaderFontFace g_reader_font_face = ReaderFontFace::SANS; +static ReaderFontFace g_reader_font_face = ReaderFontFace::BOOK; static char g_title[512] = {}; static int g_extract_len = 0; static int g_base_font_size = 18; static int g_base_title_size = 32; static int g_base_section_size = 24; +static Color g_accent = colors::ACCENT; +static bool g_welcome_feature_loaded = false; +static char g_welcome_feature_title[256] = {}; +static char g_welcome_feature_extract[1024] = {}; +static char g_welcome_status[192] = "Loading today's featured article..."; +static char g_welcome_date[16] = {}; // Large buffers — heap allocated in _start static WikiLine* g_lines = nullptr; @@ -140,7 +147,10 @@ struct ReaderToolbarLayout { Rect search_button; Rect text_size_buttons[READER_TEXT_SIZE_COUNT]; Rect font_buttons[READER_FONT_FACE_COUNT]; + Rect text_size_label; + Rect font_face_label; int sep_x; + bool show_labels; }; static void apply_reader_preferences(); @@ -178,18 +188,18 @@ static ReaderPalette reader_palette() { return { WHITE, WHITE, - Color::from_rgb(0xF6, 0xF7, 0xF9), - Color::from_rgb(0xD5, 0xDB, 0xE3), - Color::from_rgb(0x20, 0x23, 0x29), - Color::from_rgb(0x11, 0x14, 0x18), - Color::from_rgb(0x7A, 0x84, 0x92), - Color::from_rgb(0x33, 0x66, 0xCC), - Color::from_rgb(0x24, 0x52, 0xAF), - Color::from_rgb(0xE5, 0xEE, 0xFF), - Color::from_rgb(0xC8, 0x32, 0x32), - Color::from_rgb(0xA9, 0x24, 0x24), - Color::from_rgb(0xE7, 0xEB, 0xF0), - Color::from_rgb(0x97, 0x9E, 0xA8), + Color::from_rgb(0xF5, 0xF5, 0xF5), + colors::BORDER, + colors::TEXT_COLOR, + Color::from_rgb(0x22, 0x22, 0x22), + Color::from_rgb(0x77, 0x77, 0x77), + g_accent, + mtk::darken(g_accent, 32), + mtk::lighten(g_accent, 214), + Color::from_rgb(0xD0, 0x3E, 0x3E), + Color::from_rgb(0xDD, 0x44, 0x44), + Color::from_rgb(0xCC, 0xCC, 0xCC), + WHITE, }; } @@ -258,17 +268,17 @@ static int display_toggle_button_w() { } static int search_row_y() { - return 8; + return 6; } static int reader_row_y() { - return search_row_y() + search_control_h() + 8; + return search_row_y() + search_control_h() + 6; } static void update_toolbar_height() { - int h = search_row_y() + search_control_h() + 8; + int h = search_row_y() + search_control_h() + 6; if (g_display_controls_open) - h += reader_button_h() + 8; + h += reader_button_h() + 6; TOOLBAR_H = h; } @@ -283,7 +293,7 @@ static void reload_display_icons() { } static int segmented_button_w(const char* label, int min_w) { - return gui_max(text_width(label) + 18, min_w); + return gui_max(text_width(label) + 16, min_w); } static int button_group_width(const char* const* labels, int count, int min_w, int gap) { @@ -313,8 +323,9 @@ static ReaderToolbarLayout toolbar_layout() { int search_h = search_control_h(); int reader_h = reader_button_h(); int display_w = display_toggle_button_w(); + int search_btn_w = 104; - lo.search_button = {g_win_w - pad - 108, search_row_y(), 108, search_h}; + lo.search_button = {g_win_w - pad - search_btn_w, search_row_y(), search_btn_w, search_h}; lo.display_button = {lo.search_button.x - 8 - display_w, search_row_y(), display_w, search_h}; int field_w = lo.display_button.x - pad - 8; if (field_w < 80) field_w = 80; @@ -323,19 +334,42 @@ static ReaderToolbarLayout toolbar_layout() { if (!g_display_controls_open) return lo; - int gap = 6; - int group_gap = 20; - int size_w = button_group_width(g_text_size_labels, READER_TEXT_SIZE_COUNT, 34, gap); - int font_w = button_group_width(g_font_face_labels, READER_FONT_FACE_COUNT, 50, gap); - int total_w = size_w + font_w + group_gap; - int x = gui_max((g_win_w - total_w) / 2, pad); - int y = reader_row_y(); + int gap = 4; + int group_gap = 18; + int label_gap = 8; + int size_w = button_group_width(g_text_size_labels, READER_TEXT_SIZE_COUNT, 36, gap); + int font_w = button_group_width(g_font_face_labels, READER_FONT_FACE_COUNT, 54, gap); + int text_label_w = text_width("Text"); + int font_label_w = text_width("Font"); + int available_w = g_win_w - pad * 2; + int total_w = text_label_w + label_gap + size_w + group_gap + + font_label_w + label_gap + font_w; + lo.show_labels = total_w <= available_w; + if (!lo.show_labels) { + total_w = size_w + group_gap + font_w; + text_label_w = 0; + font_label_w = 0; + } - layout_button_group(lo.text_size_buttons, READER_TEXT_SIZE_COUNT, g_text_size_labels, 34, gap, &x, y, reader_h); + int x = total_w < available_w ? (g_win_w - total_w) / 2 : pad; + int y = reader_row_y(); + int label_y = y + (reader_h - system_font_height()) / 2; + + if (lo.show_labels) { + lo.text_size_label = {x, label_y, text_label_w, system_font_height()}; + x += text_label_w + label_gap; + } + + layout_button_group(lo.text_size_buttons, READER_TEXT_SIZE_COUNT, g_text_size_labels, 36, gap, &x, y, reader_h); x += group_gap; lo.sep_x = x - group_gap / 2; - layout_button_group(lo.font_buttons, READER_FONT_FACE_COUNT, g_font_face_labels, 50, gap, &x, y, reader_h); + if (lo.show_labels) { + lo.font_face_label = {x, label_y, font_label_w, system_font_height()}; + x += font_label_w + label_gap; + } + + layout_button_group(lo.font_buttons, READER_FONT_FACE_COUNT, g_font_face_labels, 54, gap, &x, y, reader_h); return lo; } @@ -377,10 +411,14 @@ static void draw_display_toggle_button(Canvas& canvas, return; } - const char* fallback = "Aa"; - int tw = text_width(fallback); - int ty = bounds.y + (bounds.h - system_font_height()) / 2; - canvas.text(bounds.x + (bounds.w - tw) / 2, ty, fallback, colors.fg); + int glyph_w = gui_min(bounds.w - 10, 16); + int glyph_h = 12; + int gx = bounds.x + (bounds.w - glyph_w) / 2; + int gy = bounds.y + (bounds.h - glyph_h) / 2; + canvas.fill_rect(gx, gy, glyph_w - 4, 2, colors.fg); + canvas.fill_rect(gx, gy + 5, glyph_w, 2, colors.fg); + canvas.fill_rect(gx, gy + 10, glyph_w - 6, 2, colors.fg); + canvas.fill_rect(gx + glyph_w - 3, gy - 1, 3, 4, colors.fg); } static int visible_line_count() { @@ -390,8 +428,11 @@ static int visible_line_count() { } static int max_scroll_lines() { - int max_scroll = g_line_count - visible_line_count(); - return max_scroll > 0 ? max_scroll : 0; + return mtk::scrollbar_max_offset(g_line_count, visible_line_count()); +} + +static bool document_scrollable() { + return g_phase == AppPhase::WELCOME || g_phase == AppPhase::DONE; } static void clamp_scroll() { @@ -407,12 +448,7 @@ static Rect scrollbar_track_rect() { int visible = visible_line_count(); if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0}; - return { - g_win_w - SCROLLBAR_W - 4, - content.y + 8, - SCROLLBAR_W, - gui_max(content.h - 16, 0) - }; + return mtk::scrollbar_track_rect(content); } static Rect scrollbar_thumb_rect() { @@ -422,12 +458,7 @@ static Rect scrollbar_thumb_rect() { int visible = visible_line_count(); if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0}; - int max_sc = max_scroll_lines(); - int thumb_h = (visible * track.h) / g_line_count; - thumb_h = gui_clamp(thumb_h, 24, track.h); - int travel = gui_max(track.h - thumb_h, 0); - int thumb_y = track.y + (max_sc > 0 ? (g_scroll_y * travel) / max_sc : 0); - return {track.x + 2, thumb_y, track.w - 4, thumb_h}; + return mtk::scrollbar_thumb_rect(track, g_line_count, visible, g_scroll_y); } static void set_scroll_from_thumb_top(int thumb_top) { @@ -435,15 +466,11 @@ static void set_scroll_from_thumb_top(int thumb_top) { Rect thumb = scrollbar_thumb_rect(); if (track.empty() || thumb.empty()) return; - int max_sc = max_scroll_lines(); - int travel = gui_max(track.h - thumb.h, 0); - if (max_sc <= 0 || travel <= 0) { - g_scroll_y = 0; - return; - } - - int rel = gui_clamp(thumb_top - track.y, 0, travel); - g_scroll_y = (rel * max_sc + travel / 2) / travel; + g_scroll_y = mtk::scrollbar_offset_from_thumb_top(track, + g_line_count, + visible_line_count(), + thumb.h, + thumb_top); clamp_scroll(); } @@ -530,6 +557,28 @@ static void append_codepoint(char* out, int* j, int maxOut, unsigned codepoint) } } +static void underscores_to_spaces(char* text) { + if (!text) return; + for (int i = 0; text[i]; i++) { + if (text[i] == '_') text[i] = ' '; + } +} + +static bool is_main_page_query(const char* query) { + if (!query) return false; + + char normalized[16]; + int out = 0; + for (int i = 0; query[i] && out < (int)sizeof(normalized) - 1; i++) { + char ch = query[i]; + if (ch == ' ' || ch == '_' || ch == '-') continue; + if (ch >= 'A' && ch <= 'Z') ch = (char)(ch - 'A' + 'a'); + normalized[out++] = ch; + } + normalized[out] = '\0'; + return strcmp(normalized, "mainpage") == 0; +} + static unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed) { if (!buf || len <= 0) { if (consumed) *consumed = 0; @@ -598,6 +647,15 @@ static int parse_status_code(const char* buf, int len) { return (buf[i]-'0')*100 + (buf[i+1]-'0')*10 + (buf[i+2]-'0'); } +static int find_substr(const char* buf, int len, const char* needle) { + int nlen = (int)strlen(needle); + if (!buf || !needle || nlen <= 0 || nlen > len) return -1; + for (int i = 0; i <= len - nlen; i++) { + if (memcmp(buf + i, needle, nlen) == 0) return i; + } + return -1; +} + // ============================================================================ // URL encoding // ============================================================================ @@ -834,16 +892,106 @@ static void build_display_lines(const char* title, const char* extract, int extr } } -static void rebuild_article(bool preserve_scroll = true) { +static int document_top_pad() { + return g_phase == AppPhase::WELCOME ? 24 : 8; +} + +static Rect document_column_rect() { + Rect column = article_column_rect(); + if (g_phase == AppPhase::WELCOME) { + int inset = gui_min(24, column.w / 12); + if (column.w - inset * 2 >= 320) { + column.x += inset; + column.w -= inset * 2; + } + } + return column; +} + +static void build_welcome_lines(bool preserve_scroll = false) { + ReaderPalette palette = reader_palette(); + TrueTypeFont* body_font = current_body_font(); + TrueTypeFont* heading_font = current_heading_font(); + int old_scroll = g_scroll_y; + + if (!body_font) return; + + g_line_count = 0; + g_scroll_y = 0; + + Rect column = document_column_rect(); + int max_px = column.w; + TrueTypeFont* tf = heading_font ? heading_font : body_font; + + const char* title = "Welcome to the Montauk Wikipedia reader"; + wrap_text(tf, TITLE_SIZE, title, (int)strlen(title), max_px, palette.heading); + add_spacer(gui_max(FONT_SIZE - 4, 10)); + + const char* intro = + "Search for an article above, or use this page as a calmer starting point than the Wikipedia Main Page."; + wrap_text(body_font, FONT_SIZE, intro, (int)strlen(intro), max_px, palette.text); + add_spacer(gui_max(FONT_SIZE + 2, 16)); + + if (g_welcome_feature_loaded && g_welcome_feature_title[0]) { + wrap_text(tf, SECTION_SIZE, "Featured article today", + (int)strlen("Featured article today"), max_px, palette.heading); + add_spacer(gui_max(FONT_SIZE - 6, 10)); + + wrap_text(tf, gui_max(FONT_SIZE + 2, 16), + g_welcome_feature_title, + (int)strlen(g_welcome_feature_title), + max_px, palette.accent); + add_spacer(gui_max(FONT_SIZE - 4, 10)); + + if (g_welcome_feature_extract[0]) { + wrap_text(body_font, FONT_SIZE, + g_welcome_feature_extract, + (int)strlen(g_welcome_feature_extract), + max_px, palette.text); + add_spacer(gui_max(FONT_SIZE - 6, 8)); + } + + char hint[360]; + snprintf(hint, sizeof(hint), + "To open the full article, search for \"%s\".", + g_welcome_feature_title); + add_spacer(gui_max(FONT_SIZE - 8, 8)); + wrap_text(body_font, gui_max(FONT_SIZE - 2, 12), + hint, (int)strlen(hint), max_px, palette.muted); + } else { + const char* section = "Start reading"; + wrap_text(tf, SECTION_SIZE, section, (int)strlen(section), max_px, palette.heading); + add_spacer(gui_max(FONT_SIZE - 8, 8)); + + const char* suggestions = + "Try topics like Linux, Montauk, operating system, Norway, mathematics, music, or any article title that comes to mind."; + wrap_text(body_font, FONT_SIZE, suggestions, (int)strlen(suggestions), max_px, palette.text); + + if (g_welcome_status[0]) { + add_spacer(gui_max(FONT_SIZE - 6, 8)); + wrap_text(body_font, gui_max(FONT_SIZE - 2, 12), + g_welcome_status, + (int)strlen(g_welcome_status), + max_px, palette.muted); + } + } + + if (preserve_scroll) + g_scroll_y = gui_clamp(old_scroll, 0, max_scroll_lines()); +} + +static void rebuild_current_document(bool preserve_scroll = true) { if (g_phase == AppPhase::DONE && g_extract_len > 0) build_display_lines(g_title, g_extract_buf, g_extract_len, preserve_scroll); + else if (g_phase == AppPhase::WELCOME) + build_welcome_lines(preserve_scroll); } static void apply_reader_setting_change() { g_scroll_dragging = false; apply_reader_preferences(); update_toolbar_height(); - rebuild_article(true); + rebuild_current_document(true); clamp_scroll(); } @@ -878,22 +1026,124 @@ static bool handle_reader_option_click(int mx, int my) { // Network search (blocking) // ============================================================================ -static void do_search(const char* query) { - // Lazy TLS/DNS init +static bool ensure_wiki_tls_ready(char* err, int err_len) { if (!g_tls_ready) { g_server_ip = montauk::resolve(WIKI_HOST); if (g_server_ip == 0) { - snprintf(g_status, sizeof(g_status), - "Error: could not resolve en.wikipedia.org"); - g_phase = AppPhase::ERR; return; + if (err && err_len > 0) + snprintf(err, err_len, "Error: could not resolve en.wikipedia.org"); + return false; } g_tas = tls::load_trust_anchors(); if (g_tas.count == 0) { - snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded"); - g_phase = AppPhase::ERR; return; + if (err && err_len > 0) + snprintf(err, err_len, "Error: no CA certificates loaded"); + return false; } g_tls_ready = true; } + return true; +} + +static bool welcome_feed_path(char* path, int path_len) { + Montauk::DateTime dt {}; + montauk::gettime(&dt); + + if (dt.Year < 2001 || dt.Month < 1 || dt.Month > 12 || + dt.Day < 1 || dt.Day > 31) { + snprintf(g_welcome_status, sizeof(g_welcome_status), + "Daily featured article unavailable until the system date is set."); + return false; + } + + snprintf(g_welcome_date, sizeof(g_welcome_date), + "%04u-%02u-%02u", + (unsigned)dt.Year, (unsigned)dt.Month, (unsigned)dt.Day); + snprintf(path, path_len, + "/api/rest_v1/feed/featured/%04u/%02u/%02u", + (unsigned)dt.Year, (unsigned)dt.Month, (unsigned)dt.Day); + return true; +} + +static void do_welcome_fetch() { + char err[192] = {}; + if (!ensure_wiki_tls_ready(err, sizeof(err))) { + snprintf(g_welcome_status, sizeof(g_welcome_status), "%s", err); + build_welcome_lines(true); + return; + } + + static char path[128]; + if (!welcome_feed_path(path, sizeof(path))) { + build_welcome_lines(true); + return; + } + + int respLen = wiki_fetch(path, g_resp_buf, RESP_MAX); + if (respLen <= 0) { + snprintf(g_welcome_status, sizeof(g_welcome_status), + "Daily featured article unavailable; search is ready."); + build_welcome_lines(true); + return; + } + g_resp_buf[respLen] = '\0'; + + int headerEnd = find_header_end(g_resp_buf, respLen); + if (headerEnd < 0) { + snprintf(g_welcome_status, sizeof(g_welcome_status), + "Daily featured article unavailable; search is ready."); + build_welcome_lines(true); + return; + } + + int status = parse_status_code(g_resp_buf, headerEnd); + const char* body = g_resp_buf + headerEnd; + int bodyLen = respLen - headerEnd; + if (status < 200 || status >= 300) { + snprintf(g_welcome_status, sizeof(g_welcome_status), + "Daily featured article unavailable; search is ready."); + build_welcome_lines(true); + return; + } + + int tfa = find_substr(body, bodyLen, "\"tfa\""); + if (tfa < 0) { + snprintf(g_welcome_status, sizeof(g_welcome_status), + "Daily featured article unavailable; search is ready."); + build_welcome_lines(true); + return; + } + + int tfaLen = bodyLen - tfa; + extract_json_string(body + tfa, tfaLen, "title", + g_welcome_feature_title, + sizeof(g_welcome_feature_title)); + extract_json_string(body + tfa, tfaLen, "extract", + g_welcome_feature_extract, + sizeof(g_welcome_feature_extract)); + + underscores_to_spaces(g_welcome_feature_title); + g_welcome_feature_loaded = g_welcome_feature_title[0] != '\0'; + if (g_welcome_feature_loaded) + g_welcome_status[0] = '\0'; + else + snprintf(g_welcome_status, sizeof(g_welcome_status), + "Daily featured article unavailable; search is ready."); + + build_welcome_lines(true); +} + +static void do_search(const char* query) { + if (is_main_page_query(query)) { + g_phase = AppPhase::WELCOME; + build_welcome_lines(false); + return; + } + + if (!ensure_wiki_tls_ready(g_status, sizeof(g_status))) { + g_phase = AppPhase::ERR; + return; + } static char encoded[1024]; url_encode_title(query, encoded, sizeof(encoded)); @@ -952,7 +1202,7 @@ static void render(Canvas& canvas) { ReaderToolbarLayout lo = toolbar_layout(); Rect toolbar = {0, 0, g_win_w, TOOLBAR_H}; Rect content = content_rect(); - Rect column = article_column_rect(); + Rect column = document_column_rect(); bool searching = (g_phase == AppPhase::LOADING); // Background @@ -974,6 +1224,14 @@ static void render(Canvas& canvas) { theme); if (g_display_controls_open) { + int row_top = reader_row_y() - 4; + canvas.hline(8, row_top, gui_max(g_win_w - 16, 0), theme.border); + + if (lo.show_labels) { + canvas.text(lo.text_size_label.x, lo.text_size_label.y, "Text", theme.text_muted); + canvas.text(lo.font_face_label.x, lo.font_face_label.y, "Font", theme.text_muted); + } + for (int i = 0; i < READER_TEXT_SIZE_COUNT; i++) { mtk::draw_button(canvas, lo.text_size_buttons[i], g_text_size_labels[i], mtk::BUTTON_SECONDARY, mtk::widget_state((int)g_reader_text_size == i, @@ -1002,8 +1260,8 @@ static void render(Canvas& canvas) { render_message(canvas, content, "Searching Wikipedia...", theme.text_muted); } else if (g_phase == AppPhase::ERR) { render_message(canvas, content, g_status, theme.danger); - } else if (g_phase == AppPhase::DONE && g_line_count > 0) { - int y = content.y + 8; + } else if (document_scrollable() && g_line_count > 0) { + int y = content.y + document_top_pad(); for (int i = g_scroll_y; i < g_line_count && y < g_win_h; i++) { WikiLine& l = g_lines[i]; @@ -1021,15 +1279,10 @@ static void render(Canvas& canvas) { Rect track = scrollbar_track_rect(); if (!track.empty()) { Rect thumb = scrollbar_thumb_rect(); - Color thumb_color = (g_scroll_dragging || mouse_in_rect(thumb)) - ? theme.accent_hover - : theme.selection; - - canvas.fill_rounded_rect(track.x, track.y, track.w, track.h, - theme.radius_sm, theme.surface_alt); - canvas.rect(track.x, track.y, track.w, track.h, theme.border); - canvas.fill_rounded_rect(thumb.x, thumb.y, thumb.w, thumb.h, - theme.radius_sm, thumb_color); + mtk::draw_scrollbar(canvas, track, thumb, + mouse_in_rect(track), + g_scroll_dragging, + theme); } } @@ -1080,7 +1333,9 @@ extern "C" void _start() { if (!g_heading_fonts[i]) g_heading_fonts[i] = g_body_fonts[i]; } + g_accent = mtk::load_system_accent(); apply_scale(montauk::win_getscale()); + build_welcome_lines(false); WsWindow win; if (!win.create("Wikipedia", INIT_W, INIT_H)) @@ -1092,8 +1347,18 @@ extern "C" void _start() { host.present(); bool search_pending = false; + bool welcome_fetch_pending = true; while (true) { + if (welcome_fetch_pending) { + welcome_fetch_pending = false; + do_welcome_fetch(); + canvas = host.canvas(); + render(canvas); + host.present(); + continue; + } + Montauk::WinEvent ev; int r = win.poll(&ev); @@ -1114,7 +1379,7 @@ extern "C" void _start() { if (ev.type == 4) { apply_scale(win.scale_factor); g_scroll_dragging = false; - rebuild_article(true); + rebuild_current_document(true); } if (ev.type == 2) { @@ -1122,7 +1387,7 @@ extern "C" void _start() { g_win_w = win.width; g_win_h = win.height; g_scroll_dragging = false; - rebuild_article(true); + rebuild_current_document(true); clamp_scroll(); } @@ -1137,7 +1402,7 @@ extern "C" void _start() { mtk::text_input_key(g_query_input, g_query, (int)sizeof(g_query), ev.key); } - } else if (g_phase == AppPhase::DONE) { + } else if (document_scrollable()) { // Navigation keys int visible = visible_line_count(); int max_sc = max_scroll_lines(); @@ -1204,7 +1469,7 @@ extern "C" void _start() { g_search_focused = false; g_display_controls_open = !g_display_controls_open; update_toolbar_height(); - rebuild_article(true); + rebuild_current_document(true); clamp_scroll(); } else if (handle_reader_option_click(mx, my)) { g_search_focused = false; @@ -1221,7 +1486,7 @@ extern "C" void _start() { } // Scroll wheel - if (ev.mouse.scroll != 0 && g_phase == AppPhase::DONE) { + if (ev.mouse.scroll != 0 && document_scrollable()) { g_scroll_y += ev.mouse.scroll * 3; clamp_scroll(); } @@ -1230,11 +1495,16 @@ extern "C" void _start() { // Trigger search if requested and query is non-empty if (search_pending && g_query[0] != '\0') { search_pending = false; - g_phase = AppPhase::LOADING; - canvas = host.canvas(); - render(canvas); - host.present(); - do_search(g_query); // blocking + if (is_main_page_query(g_query)) { + g_phase = AppPhase::WELCOME; + build_welcome_lines(false); + } else { + g_phase = AppPhase::LOADING; + canvas = host.canvas(); + render(canvas); + host.present(); + do_search(g_query); // blocking + } clamp_scroll(); } diff --git a/scripts/copy_icons.sh b/scripts/copy_icons.sh index cec9d46..b57b725 100755 --- a/scripts/copy_icons.sh +++ b/scripts/copy_icons.sh @@ -27,7 +27,7 @@ ICONS=( "actions/symbolic/document-export-symbolic.svg" "actions/symbolic/edit-undo-symbolic.svg" "actions/symbolic/edit-redo-symbolic.svg" - "actions/16/dialog-text-and-font.svg" + "actions/16/text_line_spacing.svg" "actions/symbolic/format-justify-left-symbolic.svg" "actions/symbolic/format-justify-center-symbolic.svg" "actions/symbolic/format-justify-right-symbolic.svg"