feat: MTK scrollbar widget, Wikipedia app improvements

This commit is contained in:
2026-05-14 16:10:42 +02:00
parent 44f6e6f212
commit 005bd33f32
3 changed files with 477 additions and 91 deletions
+116
View File
@@ -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); 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) { inline Rect tab_rect(const Rect& bar, int count, int index) {
if (count <= 0) return {bar.x, bar.y, 0, bar.h}; if (count <= 0) return {bar.x, bar.y, 0, bar.h};
int slot_w = bar.w / count; int slot_w = bar.w / count;
+360 -90
View File
@@ -9,6 +9,7 @@
#include <montauk/string.h> #include <montauk/string.h>
#include <montauk/heap.h> #include <montauk/heap.h>
#include <gui/mtk.hpp> #include <gui/mtk.hpp>
#include <gui/mtk/settings.hpp>
#include <gui/standalone.hpp> #include <gui/standalone.hpp>
#include <gui/svg.hpp> #include <gui/svg.hpp>
#include <gui/truetype.hpp> #include <gui/truetype.hpp>
@@ -30,7 +31,7 @@ using namespace gui::colors;
static constexpr int INIT_W = 820; static constexpr int INIT_W = 820;
static constexpr int INIT_H = 580; static constexpr int INIT_H = 580;
static int TOOLBAR_H = 42; 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 FONT_SIZE = 18;
static int TITLE_SIZE = 32; static int TITLE_SIZE = 32;
static int SECTION_SIZE = 24; static int SECTION_SIZE = 24;
@@ -39,7 +40,7 @@ static constexpr int RESP_MAX = 131072;
static constexpr int MAX_LINES = 2000; static constexpr int MAX_LINES = 2000;
static const char WIKI_HOST[] = "en.wikipedia.org"; 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 // Display line
@@ -57,7 +58,7 @@ struct WikiLine {
// App state // 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 ReaderTextSize : uint8_t { SMALL = 0, MEDIUM, LARGE, COUNT };
enum class ReaderFontFace : uint8_t { SANS = 0, SERIF, BOOK, 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_TEXT_SIZE_COUNT = (int)ReaderTextSize::COUNT;
static constexpr int READER_FONT_FACE_COUNT = (int)ReaderFontFace::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 char g_query[256] = {};
static mtk::TextInputState g_query_input = {}; static mtk::TextInputState g_query_input = {};
static char g_status[256] = {}; static char g_status[256] = {};
@@ -97,12 +98,18 @@ static bool g_scroll_dragging = false;
static int g_scroll_drag_offset = 0; static int g_scroll_drag_offset = 0;
static bool g_display_controls_open = false; static bool g_display_controls_open = false;
static ReaderTextSize g_reader_text_size = ReaderTextSize::MEDIUM; 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 char g_title[512] = {};
static int g_extract_len = 0; static int g_extract_len = 0;
static int g_base_font_size = 18; static int g_base_font_size = 18;
static int g_base_title_size = 32; static int g_base_title_size = 32;
static int g_base_section_size = 24; 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 // Large buffers — heap allocated in _start
static WikiLine* g_lines = nullptr; static WikiLine* g_lines = nullptr;
@@ -140,7 +147,10 @@ struct ReaderToolbarLayout {
Rect search_button; Rect search_button;
Rect text_size_buttons[READER_TEXT_SIZE_COUNT]; Rect text_size_buttons[READER_TEXT_SIZE_COUNT];
Rect font_buttons[READER_FONT_FACE_COUNT]; Rect font_buttons[READER_FONT_FACE_COUNT];
Rect text_size_label;
Rect font_face_label;
int sep_x; int sep_x;
bool show_labels;
}; };
static void apply_reader_preferences(); static void apply_reader_preferences();
@@ -178,18 +188,18 @@ static ReaderPalette reader_palette() {
return { return {
WHITE, WHITE,
WHITE, WHITE,
Color::from_rgb(0xF6, 0xF7, 0xF9), Color::from_rgb(0xF5, 0xF5, 0xF5),
Color::from_rgb(0xD5, 0xDB, 0xE3), colors::BORDER,
Color::from_rgb(0x20, 0x23, 0x29), colors::TEXT_COLOR,
Color::from_rgb(0x11, 0x14, 0x18), Color::from_rgb(0x22, 0x22, 0x22),
Color::from_rgb(0x7A, 0x84, 0x92), Color::from_rgb(0x77, 0x77, 0x77),
Color::from_rgb(0x33, 0x66, 0xCC), g_accent,
Color::from_rgb(0x24, 0x52, 0xAF), mtk::darken(g_accent, 32),
Color::from_rgb(0xE5, 0xEE, 0xFF), mtk::lighten(g_accent, 214),
Color::from_rgb(0xC8, 0x32, 0x32), Color::from_rgb(0xD0, 0x3E, 0x3E),
Color::from_rgb(0xA9, 0x24, 0x24), Color::from_rgb(0xDD, 0x44, 0x44),
Color::from_rgb(0xE7, 0xEB, 0xF0), Color::from_rgb(0xCC, 0xCC, 0xCC),
Color::from_rgb(0x97, 0x9E, 0xA8), WHITE,
}; };
} }
@@ -258,17 +268,17 @@ static int display_toggle_button_w() {
} }
static int search_row_y() { static int search_row_y() {
return 8; return 6;
} }
static int reader_row_y() { 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() { 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) if (g_display_controls_open)
h += reader_button_h() + 8; h += reader_button_h() + 6;
TOOLBAR_H = h; TOOLBAR_H = h;
} }
@@ -283,7 +293,7 @@ static void reload_display_icons() {
} }
static int segmented_button_w(const char* label, int min_w) { 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) { 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 search_h = search_control_h();
int reader_h = reader_button_h(); int reader_h = reader_button_h();
int display_w = display_toggle_button_w(); 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}; 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; int field_w = lo.display_button.x - pad - 8;
if (field_w < 80) field_w = 80; if (field_w < 80) field_w = 80;
@@ -323,19 +334,42 @@ static ReaderToolbarLayout toolbar_layout() {
if (!g_display_controls_open) if (!g_display_controls_open)
return lo; return lo;
int gap = 6; int gap = 4;
int group_gap = 20; int group_gap = 18;
int size_w = button_group_width(g_text_size_labels, READER_TEXT_SIZE_COUNT, 34, gap); int label_gap = 8;
int font_w = button_group_width(g_font_face_labels, READER_FONT_FACE_COUNT, 50, gap); int size_w = button_group_width(g_text_size_labels, READER_TEXT_SIZE_COUNT, 36, gap);
int total_w = size_w + font_w + group_gap; int font_w = button_group_width(g_font_face_labels, READER_FONT_FACE_COUNT, 54, gap);
int x = gui_max((g_win_w - total_w) / 2, pad); int text_label_w = text_width("Text");
int y = reader_row_y(); 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; x += group_gap;
lo.sep_x = x - group_gap / 2; 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; return lo;
} }
@@ -377,10 +411,14 @@ static void draw_display_toggle_button(Canvas& canvas,
return; return;
} }
const char* fallback = "Aa"; int glyph_w = gui_min(bounds.w - 10, 16);
int tw = text_width(fallback); int glyph_h = 12;
int ty = bounds.y + (bounds.h - system_font_height()) / 2; int gx = bounds.x + (bounds.w - glyph_w) / 2;
canvas.text(bounds.x + (bounds.w - tw) / 2, ty, fallback, colors.fg); 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() { static int visible_line_count() {
@@ -390,8 +428,11 @@ static int visible_line_count() {
} }
static int max_scroll_lines() { static int max_scroll_lines() {
int max_scroll = g_line_count - visible_line_count(); return mtk::scrollbar_max_offset(g_line_count, visible_line_count());
return max_scroll > 0 ? max_scroll : 0; }
static bool document_scrollable() {
return g_phase == AppPhase::WELCOME || g_phase == AppPhase::DONE;
} }
static void clamp_scroll() { static void clamp_scroll() {
@@ -407,12 +448,7 @@ static Rect scrollbar_track_rect() {
int visible = visible_line_count(); int visible = visible_line_count();
if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0}; if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0};
return { return mtk::scrollbar_track_rect(content);
g_win_w - SCROLLBAR_W - 4,
content.y + 8,
SCROLLBAR_W,
gui_max(content.h - 16, 0)
};
} }
static Rect scrollbar_thumb_rect() { static Rect scrollbar_thumb_rect() {
@@ -422,12 +458,7 @@ static Rect scrollbar_thumb_rect() {
int visible = visible_line_count(); int visible = visible_line_count();
if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0}; if (visible <= 0 || g_line_count <= visible) return {0, 0, 0, 0};
int max_sc = max_scroll_lines(); return mtk::scrollbar_thumb_rect(track, g_line_count, visible, g_scroll_y);
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};
} }
static void set_scroll_from_thumb_top(int thumb_top) { 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(); Rect thumb = scrollbar_thumb_rect();
if (track.empty() || thumb.empty()) return; if (track.empty() || thumb.empty()) return;
int max_sc = max_scroll_lines(); g_scroll_y = mtk::scrollbar_offset_from_thumb_top(track,
int travel = gui_max(track.h - thumb.h, 0); g_line_count,
if (max_sc <= 0 || travel <= 0) { visible_line_count(),
g_scroll_y = 0; thumb.h,
return; thumb_top);
}
int rel = gui_clamp(thumb_top - track.y, 0, travel);
g_scroll_y = (rel * max_sc + travel / 2) / travel;
clamp_scroll(); 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) { static unsigned decode_utf8_codepoint(const char* buf, int len, int* consumed) {
if (!buf || len <= 0) { if (!buf || len <= 0) {
if (consumed) *consumed = 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'); 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 // 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) if (g_phase == AppPhase::DONE && g_extract_len > 0)
build_display_lines(g_title, g_extract_buf, g_extract_len, preserve_scroll); 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() { static void apply_reader_setting_change() {
g_scroll_dragging = false; g_scroll_dragging = false;
apply_reader_preferences(); apply_reader_preferences();
update_toolbar_height(); update_toolbar_height();
rebuild_article(true); rebuild_current_document(true);
clamp_scroll(); clamp_scroll();
} }
@@ -878,22 +1026,124 @@ static bool handle_reader_option_click(int mx, int my) {
// Network search (blocking) // Network search (blocking)
// ============================================================================ // ============================================================================
static void do_search(const char* query) { static bool ensure_wiki_tls_ready(char* err, int err_len) {
// Lazy TLS/DNS init
if (!g_tls_ready) { if (!g_tls_ready) {
g_server_ip = montauk::resolve(WIKI_HOST); g_server_ip = montauk::resolve(WIKI_HOST);
if (g_server_ip == 0) { if (g_server_ip == 0) {
snprintf(g_status, sizeof(g_status), if (err && err_len > 0)
"Error: could not resolve en.wikipedia.org"); snprintf(err, err_len, "Error: could not resolve en.wikipedia.org");
g_phase = AppPhase::ERR; return; return false;
} }
g_tas = tls::load_trust_anchors(); g_tas = tls::load_trust_anchors();
if (g_tas.count == 0) { if (g_tas.count == 0) {
snprintf(g_status, sizeof(g_status), "Error: no CA certificates loaded"); if (err && err_len > 0)
g_phase = AppPhase::ERR; return; snprintf(err, err_len, "Error: no CA certificates loaded");
return false;
} }
g_tls_ready = true; 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]; static char encoded[1024];
url_encode_title(query, encoded, sizeof(encoded)); url_encode_title(query, encoded, sizeof(encoded));
@@ -952,7 +1202,7 @@ static void render(Canvas& canvas) {
ReaderToolbarLayout lo = toolbar_layout(); ReaderToolbarLayout lo = toolbar_layout();
Rect toolbar = {0, 0, g_win_w, TOOLBAR_H}; Rect toolbar = {0, 0, g_win_w, TOOLBAR_H};
Rect content = content_rect(); Rect content = content_rect();
Rect column = article_column_rect(); Rect column = document_column_rect();
bool searching = (g_phase == AppPhase::LOADING); bool searching = (g_phase == AppPhase::LOADING);
// Background // Background
@@ -974,6 +1224,14 @@ static void render(Canvas& canvas) {
theme); theme);
if (g_display_controls_open) { 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++) { 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::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, 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); render_message(canvas, content, "Searching Wikipedia...", theme.text_muted);
} else if (g_phase == AppPhase::ERR) { } else if (g_phase == AppPhase::ERR) {
render_message(canvas, content, g_status, theme.danger); render_message(canvas, content, g_status, theme.danger);
} else if (g_phase == AppPhase::DONE && g_line_count > 0) { } else if (document_scrollable() && g_line_count > 0) {
int y = content.y + 8; int y = content.y + document_top_pad();
for (int i = g_scroll_y; i < g_line_count && y < g_win_h; i++) { for (int i = g_scroll_y; i < g_line_count && y < g_win_h; i++) {
WikiLine& l = g_lines[i]; WikiLine& l = g_lines[i];
@@ -1021,15 +1279,10 @@ static void render(Canvas& canvas) {
Rect track = scrollbar_track_rect(); Rect track = scrollbar_track_rect();
if (!track.empty()) { if (!track.empty()) {
Rect thumb = scrollbar_thumb_rect(); Rect thumb = scrollbar_thumb_rect();
Color thumb_color = (g_scroll_dragging || mouse_in_rect(thumb)) mtk::draw_scrollbar(canvas, track, thumb,
? theme.accent_hover mouse_in_rect(track),
: theme.selection; g_scroll_dragging,
theme);
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);
} }
} }
@@ -1080,7 +1333,9 @@ extern "C" void _start() {
if (!g_heading_fonts[i]) g_heading_fonts[i] = g_body_fonts[i]; if (!g_heading_fonts[i]) g_heading_fonts[i] = g_body_fonts[i];
} }
g_accent = mtk::load_system_accent();
apply_scale(montauk::win_getscale()); apply_scale(montauk::win_getscale());
build_welcome_lines(false);
WsWindow win; WsWindow win;
if (!win.create("Wikipedia", INIT_W, INIT_H)) if (!win.create("Wikipedia", INIT_W, INIT_H))
@@ -1092,8 +1347,18 @@ extern "C" void _start() {
host.present(); host.present();
bool search_pending = false; bool search_pending = false;
bool welcome_fetch_pending = true;
while (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; Montauk::WinEvent ev;
int r = win.poll(&ev); int r = win.poll(&ev);
@@ -1114,7 +1379,7 @@ extern "C" void _start() {
if (ev.type == 4) { if (ev.type == 4) {
apply_scale(win.scale_factor); apply_scale(win.scale_factor);
g_scroll_dragging = false; g_scroll_dragging = false;
rebuild_article(true); rebuild_current_document(true);
} }
if (ev.type == 2) { if (ev.type == 2) {
@@ -1122,7 +1387,7 @@ extern "C" void _start() {
g_win_w = win.width; g_win_w = win.width;
g_win_h = win.height; g_win_h = win.height;
g_scroll_dragging = false; g_scroll_dragging = false;
rebuild_article(true); rebuild_current_document(true);
clamp_scroll(); clamp_scroll();
} }
@@ -1137,7 +1402,7 @@ extern "C" void _start() {
mtk::text_input_key(g_query_input, g_query, mtk::text_input_key(g_query_input, g_query,
(int)sizeof(g_query), ev.key); (int)sizeof(g_query), ev.key);
} }
} else if (g_phase == AppPhase::DONE) { } else if (document_scrollable()) {
// Navigation keys // Navigation keys
int visible = visible_line_count(); int visible = visible_line_count();
int max_sc = max_scroll_lines(); int max_sc = max_scroll_lines();
@@ -1204,7 +1469,7 @@ extern "C" void _start() {
g_search_focused = false; g_search_focused = false;
g_display_controls_open = !g_display_controls_open; g_display_controls_open = !g_display_controls_open;
update_toolbar_height(); update_toolbar_height();
rebuild_article(true); rebuild_current_document(true);
clamp_scroll(); clamp_scroll();
} else if (handle_reader_option_click(mx, my)) { } else if (handle_reader_option_click(mx, my)) {
g_search_focused = false; g_search_focused = false;
@@ -1221,7 +1486,7 @@ extern "C" void _start() {
} }
// Scroll wheel // 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; g_scroll_y += ev.mouse.scroll * 3;
clamp_scroll(); clamp_scroll();
} }
@@ -1230,11 +1495,16 @@ extern "C" void _start() {
// Trigger search if requested and query is non-empty // Trigger search if requested and query is non-empty
if (search_pending && g_query[0] != '\0') { if (search_pending && g_query[0] != '\0') {
search_pending = false; search_pending = false;
g_phase = AppPhase::LOADING; if (is_main_page_query(g_query)) {
canvas = host.canvas(); g_phase = AppPhase::WELCOME;
render(canvas); build_welcome_lines(false);
host.present(); } else {
do_search(g_query); // blocking g_phase = AppPhase::LOADING;
canvas = host.canvas();
render(canvas);
host.present();
do_search(g_query); // blocking
}
clamp_scroll(); clamp_scroll();
} }
+1 -1
View File
@@ -27,7 +27,7 @@ ICONS=(
"actions/symbolic/document-export-symbolic.svg" "actions/symbolic/document-export-symbolic.svg"
"actions/symbolic/edit-undo-symbolic.svg" "actions/symbolic/edit-undo-symbolic.svg"
"actions/symbolic/edit-redo-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-left-symbolic.svg"
"actions/symbolic/format-justify-center-symbolic.svg" "actions/symbolic/format-justify-center-symbolic.svg"
"actions/symbolic/format-justify-right-symbolic.svg" "actions/symbolic/format-justify-right-symbolic.svg"