From 047e20c8549a38c667d57607a5cd3277c70e8c46 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:46:36 +0000 Subject: [PATCH 1/8] Initial plan From 264ee82ad23c1e229e7457184472f78ba629edac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:54:30 +0000 Subject: [PATCH 2/8] Add configurable idle screen dimming Co-authored-by: Ashex <4143436+Ashex@users.noreply.github.com> --- README.md | 2 + src/kobo/main.cpp | 174 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 163 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2acd251..d8db876 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A [Bluesky](https://bsky.app) (ATProto) social client for the **Kobo Clara Colou - Quote post and external link rendering - Repost attribution display - Settings persistence via INI config file +- Configurable idle screen dim timeout with tap-to-wake - Optimised for 1448×1072 e-ink display with MXCFB partial refresh ## Getting Started @@ -222,6 +223,7 @@ refresh_jwt= did=did:plc:... pds_url=https://morel.us-east.host.bsky.network images_enabled=false +screen_dim_timeout_seconds=300 ``` All fields except `images_enabled` are written automatically by the login flow diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index 825e401..060f72d 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -89,6 +89,7 @@ constexpr const char* kEmojiLike = "💜"; constexpr const char* kEmojiReply = "💬"; constexpr const char* kEmojiRepost = "🌀"; constexpr const char* kButtonSignOut = "Log Out"; +constexpr int kDefaultScreenDimTimeoutSeconds = 300; enum class skeets_view_mode_t { dashboard, @@ -217,6 +218,9 @@ struct skeets_app_t { bool allow_nudity_content = false; bool allow_porn_content = false; bool allow_suggestive_content = false; + int screen_dim_timeout_seconds = kDefaultScreenDimTimeoutSeconds; + bool screen_dimmed = false; + std::chrono::steady_clock::time_point last_user_activity_at = std::chrono::steady_clock::now(); skeets_view_mode_t settings_return_view = skeets_view_mode_t::feed; skeets_button_t settings_back_button; skeets_button_t settings_profile_button; @@ -224,6 +228,7 @@ struct skeets_app_t { skeets_button_t settings_nudity_button; skeets_button_t settings_porn_button; skeets_button_t settings_suggestive_button; + skeets_button_t settings_screen_dim_button; skeets_button_t settings_diagnostics_button; skeets_button_t settings_sign_out_button; }; @@ -248,6 +253,36 @@ std::string bool_label(bool value) { return value ? "yes" : "no"; } +std::string format_screen_dim_timeout(int seconds) { + if (seconds <= 0) { + return "Off"; + } + if (seconds < 60) { + return std::to_string(seconds) + " sec"; + } + + const int minutes = seconds / 60; + if ((seconds % 60) == 0) { + return std::to_string(minutes) + " min"; + } + + return std::to_string(minutes) + " min " + std::to_string(seconds % 60) + " sec"; +} + +int next_screen_dim_timeout_seconds(int current) { + constexpr int kTimeoutOptions[] = {0, 30, 60, 300, 600, 1800}; + for (const int candidate : kTimeoutOptions) { + if (candidate > current) { + return candidate; + } + } + return kTimeoutOptions[0]; +} + +void mark_user_activity(skeets_app_t& app) { + app.last_user_activity_at = std::chrono::steady_clock::now(); +} + skeets_text_style_t text_style(skeets_text_role_t role) { switch (role) { case skeets_text_role_t::author: @@ -1312,6 +1347,10 @@ void load_settings(skeets_app_t& app) { app.allow_nudity_content = config_get_bool(config, "allow_nudity_content", false); app.allow_porn_content = config_get_bool(config, "allow_porn_content", false); app.allow_suggestive_content = config_get_bool(config, "allow_suggestive_content", false); + app.screen_dim_timeout_seconds = std::max(0, + config_get_int(config, + "screen_dim_timeout_seconds", + kDefaultScreenDimTimeoutSeconds)); config_free(config); } @@ -1325,6 +1364,7 @@ void save_settings(const skeets_app_t& app) { config_set_bool(config, "allow_nudity_content", app.allow_nudity_content); config_set_bool(config, "allow_porn_content", app.allow_porn_content); config_set_bool(config, "allow_suggestive_content", app.allow_suggestive_content); + config_set_int(config, "screen_dim_timeout_seconds", std::max(0, app.screen_dim_timeout_seconds)); config_save(config); config_free(config); } @@ -2620,14 +2660,59 @@ void draw_settings_action_row(skeets_app_t& app, kColorButtonSecondary); } +void draw_settings_value_row(skeets_app_t& app, + const skeets_button_t& row, + const std::string& label, + const std::string& detail, + const std::string& value) { + skeets_framebuffer_fill_rect(app.framebuffer, row.rect, kColorCard); + draw_border(app, row.rect, kColorPostBorder, 2); + + const int text_x = row.rect.x + kCardPadding; + const int text_y = row.rect.y + 12; + draw_text(app, text_x, text_y, label, skeets_text_role_t::settings_label, COLOR_BLACK, kColorCard); + draw_wrapped_text(app, + text_x, + text_y + line_height(skeets_text_role_t::settings_label) + 6, + row.rect.width - 220, + detail, + skeets_text_role_t::settings_detail, + kColorMeta, + kColorCard); + + const int value_width = 132; + const skeets_rect_t value_rect{row.rect.x + row.rect.width - value_width - 24, + row.rect.y + (row.rect.height - 52) / 2, + value_width, + 52}; + skeets_framebuffer_fill_rect(app.framebuffer, value_rect, kColorButtonSecondary); + draw_border(app, value_rect, COLOR_BLACK, 2); + draw_centered_text(app, + value_rect.x + value_rect.width / 2, + value_rect.y + (value_rect.height - line_height(skeets_text_role_t::button)) / 2, + value, + COLOR_BLACK, + kColorButtonSecondary, + skeets_text_role_t::button); +} + void render_settings_screen(skeets_app_t& app, bool full_refresh) { const int width = app.framebuffer.info.screen_width; const int height = app.framebuffer.info.screen_height; const int header_height = std::max(96, height / 11); - const int row_height = std::max(156, height / 8); - const int row_gap = 16; + const int row_gap = 12; const int content_width = width - (kOuterMargin * 2); const int first_row_y = header_height + kOuterMargin; + const int sign_out_height = std::max(90, height / 12); + const int sign_out_y = height - kOuterMargin - sign_out_height; + const int settings_note_y = sign_out_y - 52; + const int screen_dim_height = 72; + const int screen_dim_y = settings_note_y - row_gap - screen_dim_height; + constexpr int kPrimarySettingsRowCount = 6; + const int available_rows_height = std::max(0, screen_dim_y - first_row_y - row_gap); + const int row_height = std::max(96, + (available_rows_height - (row_gap * (kPrimarySettingsRowCount - 1))) / + std::max(1, kPrimarySettingsRowCount)); app.settings_back_button = {{kOuterMargin, 10, 160, header_height - 20}, kEmojiBack}; app.settings_profile_button = {{kOuterMargin, first_row_y, content_width, row_height}, "Profile Images"}; @@ -2636,10 +2721,8 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { app.settings_porn_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 3, content_width, row_height}, "Porn"}; app.settings_suggestive_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 4, content_width, row_height}, "Suggestive"}; app.settings_diagnostics_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 5, content_width, row_height}, "Diagnostics"}; - app.settings_sign_out_button = {{kOuterMargin, - height - kOuterMargin - std::max(90, height / 12), - content_width, - std::max(90, height / 12)}, + app.settings_screen_dim_button = {{kOuterMargin, screen_dim_y, content_width, screen_dim_height}, "Screen Dim"}; + app.settings_sign_out_button = {{kOuterMargin, sign_out_y, content_width, sign_out_height}, kButtonSignOut}; skeets_framebuffer_clear(app.framebuffer, COLOR_WHITE); @@ -2679,12 +2762,17 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { "Diagnostics", "Open the device and authentication diagnostics screen that used to appear at startup.", kEmojiOpen); + draw_settings_value_row(app, + app.settings_screen_dim_button, + "Screen Dim Timeout", + "Tap to cycle the idle timeout before the display switches to the dim wake screen.", + format_screen_dim_timeout(app.screen_dim_timeout_seconds)); draw_wrapped_text(app, kOuterMargin, - app.settings_diagnostics_button.rect.y + app.settings_diagnostics_button.rect.height + 20, + settings_note_y, content_width, - "These toggles are persisted. Adult-content categories default to hidden until enabled here.", + "These settings are persisted. Adult-content categories default to hidden until enabled here.", skeets_text_role_t::settings_detail, kColorMeta, COLOR_WHITE); @@ -2736,7 +2824,37 @@ void render_fatal_screen(skeets_framebuffer_t& framebuffer, fb_t& text_fb, const } } +void render_dimmed_screen(skeets_app_t& app, bool full_refresh) { + const skeets_rect_t screen_rect = full_screen_rect(app); + const int center_x = app.framebuffer.info.screen_width / 2; + const int center_y = app.framebuffer.info.screen_height / 2; + + skeets_framebuffer_clear(app.framebuffer, 0xD4); + draw_centered_text(app, center_x, center_y - 84, "sKeets", COLOR_BLACK, 0xD4, skeets_text_role_t::author); + draw_centered_text(app, center_x, center_y - 20, "Screen dimmed", COLOR_BLACK, 0xD4, skeets_text_role_t::card_title); + draw_centered_text(app, + center_x, + center_y + 48, + "Tap the screen or press a key to wake it.", + kColorMeta, + 0xD4, + skeets_text_role_t::status_detail); + + std::string error_message; + if (!skeets_framebuffer_refresh(app.framebuffer, + ui_refresh_mode(app, full_refresh), + screen_rect, + true, + &error_message)) { + std::fprintf(stderr, "rewrite app: dim refresh failed: %s\n", error_message.c_str()); + } +} + void render_active_view(skeets_app_t& app, bool full_refresh) { + if (app.screen_dimmed) { + render_dimmed_screen(app, full_refresh); + return; + } if (app.view_mode == skeets_view_mode_t::feed) { render_feed_screen(app, full_refresh); return; @@ -2887,31 +3005,50 @@ int main(int argc, char* argv[]) { error_message.clear(); const bool have_event = skeets_input_poll(skeets_app.input, event, 500, &error_message); if (!have_event) { - if (image_cache_redraw_needed() && + if (!skeets_app.screen_dimmed && + image_cache_redraw_needed() && (skeets_app.view_mode == skeets_view_mode_t::feed || skeets_app.view_mode == skeets_view_mode_t::thread)) { render_active_view(skeets_app, false); } - if (!error_message.empty()) { + if (!skeets_app.screen_dimmed && !error_message.empty()) { skeets_app.status_line = "Input polling error"; skeets_app.input_line = error_message; render_active_view(skeets_app, false); } const auto now = std::chrono::steady_clock::now(); + if (!skeets_app.screen_dimmed && + skeets_app.screen_dim_timeout_seconds > 0 && + now - skeets_app.last_user_activity_at >= + std::chrono::seconds(skeets_app.screen_dim_timeout_seconds)) { + skeets_app.screen_dimmed = true; + render_dimmed_screen(skeets_app, false); + } if (now - last_probe >= std::chrono::seconds(20)) { refresh_runtime_state(skeets_app); - if (skeets_app.view_mode != skeets_view_mode_t::feed) { + if (!skeets_app.screen_dimmed && skeets_app.view_mode != skeets_view_mode_t::feed) { skeets_app.status_line = skeets_app.bootstrap.headline; skeets_app.input_line = skeets_app.bootstrap.detail; } - render_active_view(skeets_app, false); + if (!skeets_app.screen_dimmed) { + render_active_view(skeets_app, false); + } last_probe = now; } continue; } + mark_user_activity(skeets_app); + if (skeets_app.screen_dimmed) { + skeets_app.screen_dimmed = false; + skeets_app.status_line = "Screen restored"; + skeets_app.input_line = "Resumed after the idle dim screen."; + render_active_view(skeets_app, true); + continue; + } + if (event.type == skeets_input_event_type_t::key_press) { if (event.key_code == KEY_POWER) { skeets_app.status_line = "Power key pressed"; @@ -3286,6 +3423,17 @@ int main(int argc, char* argv[]) { continue; } + if (contains_point(skeets_app.settings_screen_dim_button.rect, event.x, event.y)) { + skeets_app.screen_dim_timeout_seconds = + next_screen_dim_timeout_seconds(skeets_app.screen_dim_timeout_seconds); + save_settings(skeets_app); + skeets_app.status_line = "Settings updated"; + skeets_app.input_line = "Screen dim timeout set to " + + format_screen_dim_timeout(skeets_app.screen_dim_timeout_seconds); + render_settings_screen(skeets_app, true); + continue; + } + if (contains_point(skeets_app.settings_diagnostics_button.rect, event.x, event.y)) { skeets_app.view_mode = skeets_view_mode_t::diagnostics; skeets_app.status_line = "Diagnostics"; @@ -3375,4 +3523,4 @@ int main(int argc, char* argv[]) { free_brand_assets(skeets_app); skeets_framebuffer_close(skeets_app.framebuffer); return 0; -} \ No newline at end of file +} From 72f60b63cb89a346d5f4c3fb0464db05dc52ca30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 07:59:32 +0000 Subject: [PATCH 3/8] Refine dim wake behavior Co-authored-by: Ashex <4143436+Ashex@users.noreply.github.com> --- src/kobo/main.cpp | 61 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index 060f72d..80895c2 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -90,6 +90,10 @@ constexpr const char* kEmojiReply = "💬"; constexpr const char* kEmojiRepost = "🌀"; constexpr const char* kButtonSignOut = "Log Out"; constexpr int kDefaultScreenDimTimeoutSeconds = 300; +constexpr int kMinSettingsRowHeight = 96; +constexpr int kSettingsNoteHeight = 52; +constexpr int kSettingsValueChipHeight = 52; +constexpr std::uint8_t kColorDimScreen = 0xD4; enum class skeets_view_mode_t { dashboard, @@ -220,7 +224,8 @@ struct skeets_app_t { bool allow_suggestive_content = false; int screen_dim_timeout_seconds = kDefaultScreenDimTimeoutSeconds; bool screen_dimmed = false; - std::chrono::steady_clock::time_point last_user_activity_at = std::chrono::steady_clock::now(); + bool ignore_touch_release_after_wake = false; + std::chrono::steady_clock::time_point last_user_activity_at{}; skeets_view_mode_t settings_return_view = skeets_view_mode_t::feed; skeets_button_t settings_back_button; skeets_button_t settings_profile_button; @@ -262,11 +267,12 @@ std::string format_screen_dim_timeout(int seconds) { } const int minutes = seconds / 60; - if ((seconds % 60) == 0) { + const int remainder_seconds = seconds % 60; + if (remainder_seconds == 0) { return std::to_string(minutes) + " min"; } - return std::to_string(minutes) + " min " + std::to_string(seconds % 60) + " sec"; + return std::to_string(minutes) + " min " + std::to_string(remainder_seconds) + " sec"; } int next_screen_dim_timeout_seconds(int current) { @@ -279,6 +285,10 @@ int next_screen_dim_timeout_seconds(int current) { return kTimeoutOptions[0]; } +int sanitize_screen_dim_timeout_seconds(int seconds) { + return std::max(0, seconds); +} + void mark_user_activity(skeets_app_t& app) { app.last_user_activity_at = std::chrono::steady_clock::now(); } @@ -1347,10 +1357,8 @@ void load_settings(skeets_app_t& app) { app.allow_nudity_content = config_get_bool(config, "allow_nudity_content", false); app.allow_porn_content = config_get_bool(config, "allow_porn_content", false); app.allow_suggestive_content = config_get_bool(config, "allow_suggestive_content", false); - app.screen_dim_timeout_seconds = std::max(0, - config_get_int(config, - "screen_dim_timeout_seconds", - kDefaultScreenDimTimeoutSeconds)); + app.screen_dim_timeout_seconds = sanitize_screen_dim_timeout_seconds( + config_get_int(config, "screen_dim_timeout_seconds", kDefaultScreenDimTimeoutSeconds)); config_free(config); } @@ -1364,7 +1372,9 @@ void save_settings(const skeets_app_t& app) { config_set_bool(config, "allow_nudity_content", app.allow_nudity_content); config_set_bool(config, "allow_porn_content", app.allow_porn_content); config_set_bool(config, "allow_suggestive_content", app.allow_suggestive_content); - config_set_int(config, "screen_dim_timeout_seconds", std::max(0, app.screen_dim_timeout_seconds)); + config_set_int(config, + "screen_dim_timeout_seconds", + sanitize_screen_dim_timeout_seconds(app.screen_dim_timeout_seconds)); config_save(config); config_free(config); } @@ -2682,9 +2692,9 @@ void draw_settings_value_row(skeets_app_t& app, const int value_width = 132; const skeets_rect_t value_rect{row.rect.x + row.rect.width - value_width - 24, - row.rect.y + (row.rect.height - 52) / 2, + row.rect.y + (row.rect.height - kSettingsValueChipHeight) / 2, value_width, - 52}; + kSettingsValueChipHeight}; skeets_framebuffer_fill_rect(app.framebuffer, value_rect, kColorButtonSecondary); draw_border(app, value_rect, COLOR_BLACK, 2); draw_centered_text(app, @@ -2705,12 +2715,12 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { const int first_row_y = header_height + kOuterMargin; const int sign_out_height = std::max(90, height / 12); const int sign_out_y = height - kOuterMargin - sign_out_height; - const int settings_note_y = sign_out_y - 52; + const int settings_note_y = sign_out_y - kSettingsNoteHeight; const int screen_dim_height = 72; const int screen_dim_y = settings_note_y - row_gap - screen_dim_height; constexpr int kPrimarySettingsRowCount = 6; const int available_rows_height = std::max(0, screen_dim_y - first_row_y - row_gap); - const int row_height = std::max(96, + const int row_height = std::max(kMinSettingsRowHeight, (available_rows_height - (row_gap * (kPrimarySettingsRowCount - 1))) / std::max(1, kPrimarySettingsRowCount)); @@ -2829,15 +2839,21 @@ void render_dimmed_screen(skeets_app_t& app, bool full_refresh) { const int center_x = app.framebuffer.info.screen_width / 2; const int center_y = app.framebuffer.info.screen_height / 2; - skeets_framebuffer_clear(app.framebuffer, 0xD4); - draw_centered_text(app, center_x, center_y - 84, "sKeets", COLOR_BLACK, 0xD4, skeets_text_role_t::author); - draw_centered_text(app, center_x, center_y - 20, "Screen dimmed", COLOR_BLACK, 0xD4, skeets_text_role_t::card_title); + skeets_framebuffer_clear(app.framebuffer, kColorDimScreen); + draw_centered_text(app, center_x, center_y - 84, "sKeets", COLOR_BLACK, kColorDimScreen, skeets_text_role_t::author); + draw_centered_text(app, + center_x, + center_y - 20, + "Screen dimmed", + COLOR_BLACK, + kColorDimScreen, + skeets_text_role_t::card_title); draw_centered_text(app, center_x, center_y + 48, "Tap the screen or press a key to wake it.", kColorMeta, - 0xD4, + kColorDimScreen, skeets_text_role_t::status_detail); std::string error_message; @@ -2996,6 +3012,7 @@ int main(int argc, char* argv[]) { } else { render_screen(skeets_app, true); } + mark_user_activity(skeets_app); auto last_probe = std::chrono::steady_clock::now(); while (!g_stop_requested) { @@ -3043,8 +3060,11 @@ int main(int argc, char* argv[]) { mark_user_activity(skeets_app); if (skeets_app.screen_dimmed) { skeets_app.screen_dimmed = false; - skeets_app.status_line = "Screen restored"; - skeets_app.input_line = "Resumed after the idle dim screen."; + // A touch-down wake will still be followed by the touch release from the same tap. + // Swallow that release so waking the screen does not also activate the underlying UI. + skeets_app.ignore_touch_release_after_wake = + event.type == skeets_input_event_type_t::touch_down || + event.type == skeets_input_event_type_t::touch_move; render_active_view(skeets_app, true); continue; } @@ -3067,6 +3087,11 @@ int main(int argc, char* argv[]) { continue; } + if (skeets_app.ignore_touch_release_after_wake) { + skeets_app.ignore_touch_release_after_wake = false; + continue; + } + std::ostringstream touch_message; touch_message << "Touch released at " << event.x << "," << event.y; From 5ddb075aa445dc89d5511c8513b829dd7f77c568 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:21:25 +0000 Subject: [PATCH 4/8] Use frontlight-only idle dimming Co-authored-by: Ashex <4143436+Ashex@users.noreply.github.com> --- README.md | 2 +- src/kobo/main.cpp | 188 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 148 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index d8db876..74e3064 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A [Bluesky](https://bsky.app) (ATProto) social client for the **Kobo Clara Colou - Quote post and external link rendering - Repost attribution display - Settings persistence via INI config file -- Configurable idle screen dim timeout with tap-to-wake +- Configurable idle frontlight-off timeout with wake-on-input - Optimised for 1448×1072 e-ink display with MXCFB partial refresh ## Getting Started diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index 80895c2..25d44ac 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -25,11 +25,15 @@ #include #include +#include #include #include #include #include +#include +#include #include +#include #include #include #include @@ -90,10 +94,12 @@ constexpr const char* kEmojiReply = "💬"; constexpr const char* kEmojiRepost = "🌀"; constexpr const char* kButtonSignOut = "Log Out"; constexpr int kDefaultScreenDimTimeoutSeconds = 300; +constexpr int kBacklightPowerOffValue = 0; +// Kobo frontlight sysfs uses bl_power=31 while the light rail is enabled. +constexpr int kBacklightPowerOnValue = 31; constexpr int kMinSettingsRowHeight = 96; constexpr int kSettingsNoteHeight = 52; constexpr int kSettingsValueChipHeight = 52; -constexpr std::uint8_t kColorDimScreen = 0xD4; enum class skeets_view_mode_t { dashboard, @@ -225,6 +231,9 @@ struct skeets_app_t { int screen_dim_timeout_seconds = kDefaultScreenDimTimeoutSeconds; bool screen_dimmed = false; bool ignore_touch_release_after_wake = false; + bool frontlight_control_available = true; + std::string frontlight_brightness_path; + int saved_frontlight_brightness = -1; std::chrono::steady_clock::time_point last_user_activity_at{}; skeets_view_mode_t settings_return_view = skeets_view_mode_t::feed; skeets_button_t settings_back_button; @@ -289,6 +298,134 @@ int sanitize_screen_dim_timeout_seconds(int seconds) { return std::max(0, seconds); } +bool path_exists(const std::string& path) { + std::error_code error; + return std::filesystem::exists(path, error); +} + +std::optional read_int_file(const std::string& path) { + std::ifstream file(path); + if (!file) { + return std::nullopt; + } + + std::string value_text; + std::getline(file, value_text); + if (!file && value_text.empty()) { + return std::nullopt; + } + + size_t parsed_length = 0; + int value = 0; + try { + value = std::stoi(value_text, &parsed_length); + } catch (const std::exception&) { + return std::nullopt; + } + + if (std::any_of(value_text.begin() + static_cast(parsed_length), + value_text.end(), + [](char ch) { return !std::isspace(static_cast(ch)); })) { + return std::nullopt; + } + return value; +} + +bool write_text_file(const std::string& path, const std::string& value) { + std::ofstream file(path); + if (!file) { + return false; + } + file << value; + return static_cast(file); +} + +std::string brightness_bl_power_path(const std::string& brightness_path) { + const std::filesystem::path sysfs_path(brightness_path); + if (!sysfs_path.has_parent_path()) { + return {}; + } + return (sysfs_path.parent_path() / "bl_power").string(); +} + +std::string detect_frontlight_brightness_path() { + if (const char* env_path = std::getenv("SKEETS_FRONTLIGHT_BRIGHTNESS_SYSFS"); env_path && *env_path) { + return env_path; + } + + constexpr const char* kCandidates[] = { + "/sys/class/backlight/mxc_msp430.0/brightness", + "/sys/class/backlight/lm3630a_led/brightness", + "/sys/class/backlight/lm3630a_ledb/brightness", + "/sys/class/backlight/lm3630a_led1b/brightness", + }; + for (const char* candidate : kCandidates) { + if (path_exists(candidate)) { + return candidate; + } + } + return {}; +} + +bool set_frontlight_brightness(const std::string& brightness_path, int brightness) { + if (brightness_path.empty()) { + return false; + } + + const int nonnegative_brightness = std::max(0, brightness); + const std::string bl_power_path = brightness_bl_power_path(brightness_path); + if (path_exists(bl_power_path)) { + if (!write_text_file(bl_power_path, + std::to_string(nonnegative_brightness > 0 ? kBacklightPowerOnValue + : kBacklightPowerOffValue))) { + return false; + } + } + + return write_text_file(brightness_path, std::to_string(nonnegative_brightness)); +} + +bool dim_frontlight(skeets_app_t& app) { + if (!app.frontlight_control_available) { + return false; + } + if (app.frontlight_brightness_path.empty()) { + app.frontlight_brightness_path = detect_frontlight_brightness_path(); + } + if (app.frontlight_brightness_path.empty()) { + app.frontlight_control_available = false; + return false; + } + + const auto current_brightness = read_int_file(app.frontlight_brightness_path); + if (!current_brightness) { + app.frontlight_control_available = false; + return false; + } + if (*current_brightness <= 0) { + return false; + } + + if (!set_frontlight_brightness(app.frontlight_brightness_path, 0)) { + app.frontlight_control_available = false; + return false; + } + + app.saved_frontlight_brightness = *current_brightness; + return true; +} + +void restore_frontlight(skeets_app_t& app) { + if (app.saved_frontlight_brightness < 0 || app.frontlight_brightness_path.empty()) { + app.saved_frontlight_brightness = -1; + return; + } + + if (set_frontlight_brightness(app.frontlight_brightness_path, app.saved_frontlight_brightness)) { + app.saved_frontlight_brightness = -1; + } +} + void mark_user_activity(skeets_app_t& app) { app.last_user_activity_at = std::chrono::steady_clock::now(); } @@ -2775,7 +2912,7 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { draw_settings_value_row(app, app.settings_screen_dim_button, "Screen Dim Timeout", - "Tap to cycle the idle timeout before the display switches to the dim wake screen.", + "Tap to cycle the idle timeout before the frontlight turns off until the next input.", format_screen_dim_timeout(app.screen_dim_timeout_seconds)); draw_wrapped_text(app, @@ -2834,43 +2971,7 @@ void render_fatal_screen(skeets_framebuffer_t& framebuffer, fb_t& text_fb, const } } -void render_dimmed_screen(skeets_app_t& app, bool full_refresh) { - const skeets_rect_t screen_rect = full_screen_rect(app); - const int center_x = app.framebuffer.info.screen_width / 2; - const int center_y = app.framebuffer.info.screen_height / 2; - - skeets_framebuffer_clear(app.framebuffer, kColorDimScreen); - draw_centered_text(app, center_x, center_y - 84, "sKeets", COLOR_BLACK, kColorDimScreen, skeets_text_role_t::author); - draw_centered_text(app, - center_x, - center_y - 20, - "Screen dimmed", - COLOR_BLACK, - kColorDimScreen, - skeets_text_role_t::card_title); - draw_centered_text(app, - center_x, - center_y + 48, - "Tap the screen or press a key to wake it.", - kColorMeta, - kColorDimScreen, - skeets_text_role_t::status_detail); - - std::string error_message; - if (!skeets_framebuffer_refresh(app.framebuffer, - ui_refresh_mode(app, full_refresh), - screen_rect, - true, - &error_message)) { - std::fprintf(stderr, "rewrite app: dim refresh failed: %s\n", error_message.c_str()); - } -} - void render_active_view(skeets_app_t& app, bool full_refresh) { - if (app.screen_dimmed) { - render_dimmed_screen(app, full_refresh); - return; - } if (app.view_mode == skeets_view_mode_t::feed) { render_feed_screen(app, full_refresh); return; @@ -3037,11 +3138,15 @@ int main(int argc, char* argv[]) { const auto now = std::chrono::steady_clock::now(); if (!skeets_app.screen_dimmed && + skeets_app.frontlight_control_available && skeets_app.screen_dim_timeout_seconds > 0 && now - skeets_app.last_user_activity_at >= std::chrono::seconds(skeets_app.screen_dim_timeout_seconds)) { - skeets_app.screen_dimmed = true; - render_dimmed_screen(skeets_app, false); + if (dim_frontlight(skeets_app)) { + skeets_app.screen_dimmed = true; + } else if (skeets_app.frontlight_control_available) { + mark_user_activity(skeets_app); + } } if (now - last_probe >= std::chrono::seconds(20)) { refresh_runtime_state(skeets_app); @@ -3060,12 +3165,12 @@ int main(int argc, char* argv[]) { mark_user_activity(skeets_app); if (skeets_app.screen_dimmed) { skeets_app.screen_dimmed = false; + restore_frontlight(skeets_app); // A touch-down wake will still be followed by the touch release from the same tap. // Swallow that release so waking the screen does not also activate the underlying UI. skeets_app.ignore_touch_release_after_wake = event.type == skeets_input_event_type_t::touch_down || event.type == skeets_input_event_type_t::touch_move; - render_active_view(skeets_app, true); continue; } @@ -3543,6 +3648,7 @@ int main(int argc, char* argv[]) { render_screen(skeets_app, false); } + restore_frontlight(skeets_app); skeets_input_close(skeets_app.input); clear_embed_cache_on_exit(); free_brand_assets(skeets_app); From 4ffac912e00c0375bb876c3d0e2927861b56a5e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:48:39 +0000 Subject: [PATCH 5/8] Add settings scrollbar pagination Co-authored-by: Ashex <4143436+Ashex@users.noreply.github.com> --- src/kobo/main.cpp | 363 +++++++++++++++++++++++++++++++--------------- 1 file changed, 246 insertions(+), 117 deletions(-) diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index 25d44ac..a41a54b 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -236,6 +236,8 @@ struct skeets_app_t { int saved_frontlight_brightness = -1; std::chrono::steady_clock::time_point last_user_activity_at{}; skeets_view_mode_t settings_return_view = skeets_view_mode_t::feed; + int settings_page_start = 0; + int settings_page_count = 0; skeets_button_t settings_back_button; skeets_button_t settings_profile_button; skeets_button_t settings_embed_button; @@ -245,6 +247,10 @@ struct skeets_app_t { skeets_button_t settings_screen_dim_button; skeets_button_t settings_diagnostics_button; skeets_button_t settings_sign_out_button; + skeets_rect_t settings_scrollbar_up_rect{}; + skeets_rect_t settings_scrollbar_down_rect{}; + skeets_rect_t settings_scrollbar_rect{}; + skeets_rect_t settings_scrollbar_thumb_rect{}; }; void draw_border(skeets_app_t& app, const skeets_rect_t& rect, std::uint8_t color, int thickness = 2); @@ -1596,9 +1602,10 @@ void open_feed_list(skeets_app_t& app) { void open_settings(skeets_app_t& app, skeets_view_mode_t return_view) { app.settings_return_view = return_view; + app.settings_page_start = 0; app.view_mode = skeets_view_mode_t::settings; app.status_line = "Settings"; - app.input_line = "Toggle image and moderation preferences or sign out"; + app.input_line = "Toggle preferences, use the right scrollbar for more, or sign out"; } bool label_matches_nudity(const std::string& label) { @@ -1847,13 +1854,85 @@ int measure_thread_post_height(const skeets_app_t& app, return needed; } -int clamp_thread_page_start(int start, int total) { +int clamp_paginated_start(int start, int total) { if (total <= 0) { return 0; } return std::max(0, std::min(start, total - 1)); } +void layout_vertical_scrollbar(int width, + int header_height, + int top_y, + int bottom_y, + skeets_rect_t& up_rect, + skeets_rect_t& down_rect, + skeets_rect_t& track_rect, + skeets_rect_t& thumb_rect) { + const int scrollbar_x = width - kOuterMargin - kThreadScrollbarWidth; + const int scrollbar_end_cap_height = std::max(kThreadScrollbarEndCapHeight, header_height - 20); + const int scrollbar_track_top = top_y + scrollbar_end_cap_height + kThreadScrollbarEndCapGap; + const int scrollbar_track_bottom = bottom_y - scrollbar_end_cap_height - kThreadScrollbarEndCapGap; + const int scrollbar_height = std::max(80, scrollbar_track_bottom - scrollbar_track_top); + + up_rect = {scrollbar_x, top_y, kThreadScrollbarWidth, scrollbar_end_cap_height}; + down_rect = {scrollbar_x, bottom_y - scrollbar_end_cap_height, kThreadScrollbarWidth, scrollbar_end_cap_height}; + track_rect = {scrollbar_x, scrollbar_track_top, kThreadScrollbarWidth, scrollbar_height}; + thumb_rect = track_rect; +} + +void draw_vertical_scrollbar(skeets_app_t& app, + const skeets_rect_t& up_rect, + const skeets_rect_t& down_rect, + const skeets_rect_t& track_rect, + skeets_rect_t& thumb_rect, + int total, + int rendered, + int page_start) { + skeets_framebuffer_fill_rect(app.framebuffer, up_rect, kColorButtonSecondary); + draw_border(app, up_rect, kColorPostBorder, 1); + draw_centered_text(app, + up_rect.x + (up_rect.width / 2), + up_rect.y + std::max(0, (up_rect.height - line_height(skeets_text_role_t::meta)) / 2), + "UP", + COLOR_BLACK, + kColorButtonSecondary, + skeets_text_role_t::meta); + + skeets_framebuffer_fill_rect(app.framebuffer, down_rect, kColorButtonSecondary); + draw_border(app, down_rect, kColorPostBorder, 1); + draw_centered_text(app, + down_rect.x + (down_rect.width / 2), + down_rect.y + std::max(0, (down_rect.height - line_height(skeets_text_role_t::meta)) / 2), + "DN", + COLOR_BLACK, + kColorButtonSecondary, + skeets_text_role_t::meta); + + skeets_framebuffer_fill_rect(app.framebuffer, track_rect, 0xDD); + draw_border(app, track_rect, kColorPostBorder, 1); + + if (total <= 0 || rendered <= 0) { + thumb_rect = track_rect; + return; + } + + const int max_start = std::max(0, total - rendered); + const int thumb_height = std::max(40, + (track_rect.height * rendered) / + std::max(1, total)); + const int travel = std::max(0, track_rect.height - thumb_height); + int thumb_y = track_rect.y; + if (max_start > 0 && travel > 0) { + thumb_y += (page_start * travel) / max_start; + } + thumb_rect = {track_rect.x + 2, + thumb_y + 2, + std::max(4, track_rect.width - 4), + std::max(12, thumb_height - 4)}; + skeets_framebuffer_fill_rect(app.framebuffer, thumb_rect, kColorButtonPrimary); +} + skeets_rect_t make_stat_hit_rect(int x, int y, int width, int height) { return skeets_rect_t{x, std::max(0, y - 6), width, height + 12}; } @@ -2488,27 +2567,17 @@ void render_thread_screen(skeets_app_t& app, bool full_refresh) { const int content_bottom = height - kOuterMargin; const int line_spacing = 4; const int scrollbar_top = header_height + kOuterMargin; - const int scrollbar_x = width - kOuterMargin - kThreadScrollbarWidth; - const int scrollbar_end_cap_height = std::max(kThreadScrollbarEndCapHeight, header_height - 20); - const int scrollbar_track_top = scrollbar_top + scrollbar_end_cap_height + kThreadScrollbarEndCapGap; - const int scrollbar_track_bottom = content_bottom - scrollbar_end_cap_height - kThreadScrollbarEndCapGap; - const int scrollbar_height = std::max(80, scrollbar_track_bottom - scrollbar_track_top); app.thread_back_button = {{kOuterMargin, 10, 160, header_height - 20}, kEmojiBack}; app.thread_settings_button = {{width - kOuterMargin - 176, 10, 176, header_height - 20}, kEmojiSettings}; - app.thread_scrollbar_up_rect = {scrollbar_x, - scrollbar_top, - kThreadScrollbarWidth, - scrollbar_end_cap_height}; - app.thread_scrollbar_down_rect = {scrollbar_x, - content_bottom - scrollbar_end_cap_height, - kThreadScrollbarWidth, - scrollbar_end_cap_height}; - app.thread_scrollbar_rect = {scrollbar_x, - scrollbar_track_top, - kThreadScrollbarWidth, - scrollbar_height}; - app.thread_scrollbar_thumb_rect = app.thread_scrollbar_rect; + layout_vertical_scrollbar(width, + header_height, + scrollbar_top, + content_bottom, + app.thread_scrollbar_up_rect, + app.thread_scrollbar_down_rect, + app.thread_scrollbar_rect, + app.thread_scrollbar_thumb_rect); app.thread_post_hits.clear(); app.thread_page_count = 0; @@ -2537,7 +2606,7 @@ void render_thread_screen(skeets_app_t& app, bool full_refresh) { std::vector> nodes; flatten_thread_posts(app.thread_result.root, 0, nodes); const int total = static_cast(nodes.size()); - app.thread_page_start = clamp_thread_page_start(app.thread_page_start, total); + app.thread_page_start = clamp_paginated_start(app.thread_page_start, total); int rendered = 0; for (int index = app.thread_page_start; index < total && cursor_y < content_bottom; ++index) { const auto& node = nodes[index]; @@ -2667,53 +2736,14 @@ void render_thread_screen(skeets_app_t& app, bool full_refresh) { app.thread_page_count = rendered; - skeets_framebuffer_fill_rect(app.framebuffer, - app.thread_scrollbar_up_rect, - kColorButtonSecondary); - draw_border(app, app.thread_scrollbar_up_rect, kColorPostBorder, 1); - draw_centered_text(app, - app.thread_scrollbar_up_rect.x + (app.thread_scrollbar_up_rect.width / 2), - app.thread_scrollbar_up_rect.y + std::max(0, (app.thread_scrollbar_up_rect.height - line_height(skeets_text_role_t::meta)) / 2), - "UP", - COLOR_BLACK, - kColorButtonSecondary, - skeets_text_role_t::meta); - - skeets_framebuffer_fill_rect(app.framebuffer, - app.thread_scrollbar_down_rect, - kColorButtonSecondary); - draw_border(app, app.thread_scrollbar_down_rect, kColorPostBorder, 1); - draw_centered_text(app, - app.thread_scrollbar_down_rect.x + (app.thread_scrollbar_down_rect.width / 2), - app.thread_scrollbar_down_rect.y + std::max(0, (app.thread_scrollbar_down_rect.height - line_height(skeets_text_role_t::meta)) / 2), - "DN", - COLOR_BLACK, - kColorButtonSecondary, - skeets_text_role_t::meta); - - skeets_framebuffer_fill_rect(app.framebuffer, - app.thread_scrollbar_rect, - 0xDD); - draw_border(app, app.thread_scrollbar_rect, kColorPostBorder, 1); - - if (total > 0 && rendered > 0) { - const int max_start = std::max(0, total - rendered); - const int thumb_height = std::max(40, - (app.thread_scrollbar_rect.height * rendered) / - std::max(1, total)); - const int travel = std::max(0, app.thread_scrollbar_rect.height - thumb_height); - int thumb_y = app.thread_scrollbar_rect.y; - if (max_start > 0 && travel > 0) { - thumb_y += (app.thread_page_start * travel) / max_start; - } - app.thread_scrollbar_thumb_rect = {app.thread_scrollbar_rect.x + 2, - thumb_y + 2, - std::max(4, app.thread_scrollbar_rect.width - 4), - std::max(12, thumb_height - 4)}; - skeets_framebuffer_fill_rect(app.framebuffer, - app.thread_scrollbar_thumb_rect, - kColorButtonPrimary); - } + draw_vertical_scrollbar(app, + app.thread_scrollbar_up_rect, + app.thread_scrollbar_down_rect, + app.thread_scrollbar_rect, + app.thread_scrollbar_thumb_rect, + total, + rendered, + app.thread_page_start); } std::string error_message; @@ -2848,29 +2878,41 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { const int height = app.framebuffer.info.screen_height; const int header_height = std::max(96, height / 11); const int row_gap = 12; - const int content_width = width - (kOuterMargin * 2); + const int content_right = width - kOuterMargin - kThreadScrollbarWidth - kThreadScrollbarGap; + const int content_width = content_right - kOuterMargin; const int first_row_y = header_height + kOuterMargin; const int sign_out_height = std::max(90, height / 12); const int sign_out_y = height - kOuterMargin - sign_out_height; const int settings_note_y = sign_out_y - kSettingsNoteHeight; - const int screen_dim_height = 72; - const int screen_dim_y = settings_note_y - row_gap - screen_dim_height; - constexpr int kPrimarySettingsRowCount = 6; - const int available_rows_height = std::max(0, screen_dim_y - first_row_y - row_gap); + const int settings_content_bottom = settings_note_y - row_gap; + const int scrollbar_top = header_height + kOuterMargin; + constexpr int kVisibleSettingsRowCount = 6; + constexpr int kScrollableSettingsRowCount = 7; + const int available_rows_height = std::max(0, settings_content_bottom - first_row_y); const int row_height = std::max(kMinSettingsRowHeight, - (available_rows_height - (row_gap * (kPrimarySettingsRowCount - 1))) / - std::max(1, kPrimarySettingsRowCount)); + (available_rows_height - (row_gap * (kVisibleSettingsRowCount - 1))) / + std::max(1, kVisibleSettingsRowCount)); app.settings_back_button = {{kOuterMargin, 10, 160, header_height - 20}, kEmojiBack}; - app.settings_profile_button = {{kOuterMargin, first_row_y, content_width, row_height}, "Profile Images"}; - app.settings_embed_button = {{kOuterMargin, first_row_y + row_height + row_gap, content_width, row_height}, "Embed Images"}; - app.settings_nudity_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 2, content_width, row_height}, "Nudity"}; - app.settings_porn_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 3, content_width, row_height}, "Porn"}; - app.settings_suggestive_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 4, content_width, row_height}, "Suggestive"}; - app.settings_diagnostics_button = {{kOuterMargin, first_row_y + (row_height + row_gap) * 5, content_width, row_height}, "Diagnostics"}; - app.settings_screen_dim_button = {{kOuterMargin, screen_dim_y, content_width, screen_dim_height}, "Screen Dim"}; app.settings_sign_out_button = {{kOuterMargin, sign_out_y, content_width, sign_out_height}, kButtonSignOut}; + layout_vertical_scrollbar(width, + header_height, + scrollbar_top, + settings_content_bottom, + app.settings_scrollbar_up_rect, + app.settings_scrollbar_down_rect, + app.settings_scrollbar_rect, + app.settings_scrollbar_thumb_rect); + app.settings_profile_button.rect = {}; + app.settings_embed_button.rect = {}; + app.settings_nudity_button.rect = {}; + app.settings_porn_button.rect = {}; + app.settings_suggestive_button.rect = {}; + app.settings_diagnostics_button.rect = {}; + app.settings_screen_dim_button.rect = {}; + app.settings_page_start = clamp_paginated_start(app.settings_page_start, kScrollableSettingsRowCount); + app.settings_page_count = 0; skeets_framebuffer_clear(app.framebuffer, COLOR_WHITE); @@ -2879,41 +2921,82 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { draw_button(app, app.settings_back_button, kColorButtonSecondary); draw_header_brand(app, header_rect); - draw_settings_row(app, - app.settings_profile_button, - "Profile Images", - "Load avatar images when image rendering is available.", - app.profile_images_enabled); - draw_settings_row(app, - app.settings_embed_button, - "Embed Images", - "Show post image placeholders and enable image loading later.", - app.embed_images_enabled); - draw_settings_row(app, - app.settings_nudity_button, - "Nudity", - "Show posts labeled for nudity.", - app.allow_nudity_content); - draw_settings_row(app, - app.settings_porn_button, - "Porn", - "Show posts labeled for pornographic content.", - app.allow_porn_content); - draw_settings_row(app, - app.settings_suggestive_button, - "Suggestive", - "Show posts labeled for suggestive or sexual content.", - app.allow_suggestive_content); - draw_settings_action_row(app, - app.settings_diagnostics_button, - "Diagnostics", - "Open the device and authentication diagnostics screen that used to appear at startup.", - kEmojiOpen); - draw_settings_value_row(app, - app.settings_screen_dim_button, - "Screen Dim Timeout", - "Tap to cycle the idle timeout before the frontlight turns off until the next input.", - format_screen_dim_timeout(app.screen_dim_timeout_seconds)); + int rendered = 0; + for (int index = app.settings_page_start; + index < kScrollableSettingsRowCount && rendered < kVisibleSettingsRowCount; + ++index) { + const int row_y = first_row_y + rendered * (row_height + row_gap); + switch (index) { + case 0: + app.settings_profile_button = {{kOuterMargin, row_y, content_width, row_height}, "Profile Images"}; + draw_settings_row(app, + app.settings_profile_button, + "Profile Images", + "Load avatar images when image rendering is available.", + app.profile_images_enabled); + break; + case 1: + app.settings_embed_button = {{kOuterMargin, row_y, content_width, row_height}, "Embed Images"}; + draw_settings_row(app, + app.settings_embed_button, + "Embed Images", + "Show post image placeholders and enable image loading later.", + app.embed_images_enabled); + break; + case 2: + app.settings_nudity_button = {{kOuterMargin, row_y, content_width, row_height}, "Nudity"}; + draw_settings_row(app, + app.settings_nudity_button, + "Nudity", + "Show posts labeled for nudity.", + app.allow_nudity_content); + break; + case 3: + app.settings_porn_button = {{kOuterMargin, row_y, content_width, row_height}, "Porn"}; + draw_settings_row(app, + app.settings_porn_button, + "Porn", + "Show posts labeled for pornographic content.", + app.allow_porn_content); + break; + case 4: + app.settings_suggestive_button = {{kOuterMargin, row_y, content_width, row_height}, "Suggestive"}; + draw_settings_row(app, + app.settings_suggestive_button, + "Suggestive", + "Show posts labeled for suggestive or sexual content.", + app.allow_suggestive_content); + break; + case 5: + app.settings_diagnostics_button = {{kOuterMargin, row_y, content_width, row_height}, "Diagnostics"}; + draw_settings_action_row(app, + app.settings_diagnostics_button, + "Diagnostics", + "Open the device and authentication diagnostics screen that used to appear at startup.", + kEmojiOpen); + break; + case 6: + app.settings_screen_dim_button = {{kOuterMargin, row_y, content_width, row_height}, "Screen Dim"}; + draw_settings_value_row(app, + app.settings_screen_dim_button, + "Screen Dim Timeout", + "Tap to cycle the idle timeout before the frontlight turns off until the next input.", + format_screen_dim_timeout(app.screen_dim_timeout_seconds)); + break; + default: + break; + } + rendered++; + } + app.settings_page_count = rendered; + draw_vertical_scrollbar(app, + app.settings_scrollbar_up_rect, + app.settings_scrollbar_down_rect, + app.settings_scrollbar_rect, + app.settings_scrollbar_thumb_rect, + kScrollableSettingsRowCount, + rendered, + app.settings_page_start); draw_wrapped_text(app, kOuterMargin, @@ -3564,6 +3647,52 @@ int main(int argc, char* argv[]) { continue; } + if (contains_point(skeets_app.settings_scrollbar_up_rect, event.x, event.y)) { + if (skeets_app.settings_page_start > 0) { + const int page_step = std::max(1, skeets_app.settings_page_count - 1); + skeets_app.settings_page_start = std::max(0, skeets_app.settings_page_start - page_step); + render_settings_screen(skeets_app, true); + } else { + skeets_app.status_line = "Already at top of settings"; + skeets_app.input_line = touch_message.str(); + render_settings_screen(skeets_app, false); + } + continue; + } + + if (contains_point(skeets_app.settings_scrollbar_down_rect, event.x, event.y)) { + constexpr int kScrollableSettingsRowCount = 7; + const int max_start = std::max(0, kScrollableSettingsRowCount - std::max(1, skeets_app.settings_page_count)); + if (skeets_app.settings_page_start < max_start) { + const int page_step = std::max(1, skeets_app.settings_page_count - 1); + skeets_app.settings_page_start = std::min(max_start, skeets_app.settings_page_start + page_step); + render_settings_screen(skeets_app, true); + } else { + skeets_app.status_line = "Already at bottom of settings"; + skeets_app.input_line = touch_message.str(); + render_settings_screen(skeets_app, false); + } + continue; + } + + if (contains_point(skeets_app.settings_scrollbar_rect, event.x, event.y)) { + constexpr int kScrollableSettingsRowCount = 7; + const int max_start = std::max(0, kScrollableSettingsRowCount - std::max(1, skeets_app.settings_page_count)); + if (max_start > 0 && skeets_app.settings_scrollbar_rect.height > 1) { + const int relative_y = std::max(0, + std::min(event.y - skeets_app.settings_scrollbar_rect.y, + skeets_app.settings_scrollbar_rect.height - 1)); + skeets_app.settings_page_start = (relative_y * max_start) / + std::max(1, skeets_app.settings_scrollbar_rect.height - 1); + render_settings_screen(skeets_app, true); + } else { + skeets_app.status_line = "Settings already fit on-screen"; + skeets_app.input_line = touch_message.str(); + render_settings_screen(skeets_app, false); + } + continue; + } + if (contains_point(skeets_app.settings_diagnostics_button.rect, event.x, event.y)) { skeets_app.view_mode = skeets_view_mode_t::diagnostics; skeets_app.status_line = "Diagnostics"; From 2d7e094df3ef3c7268f09b5550c060618d501af6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 08:51:35 +0000 Subject: [PATCH 6/8] Tidy settings scrollbar constants Co-authored-by: Ashex <4143436+Ashex@users.noreply.github.com> --- src/kobo/main.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index a41a54b..d89da7d 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -65,6 +65,7 @@ constexpr int kThreadScrollbarWidth = 36; constexpr int kThreadScrollbarGap = 12; constexpr int kThreadScrollbarEndCapHeight = 72; constexpr int kThreadScrollbarEndCapGap = 10; +constexpr int kScrollableSettingsRowCount = 7; constexpr int kEmbedImageGap = 6; constexpr int kHeaderLogoMaxWidth = 280; constexpr int kHeaderLogoMaxHeight = 64; @@ -2887,7 +2888,6 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { const int settings_content_bottom = settings_note_y - row_gap; const int scrollbar_top = header_height + kOuterMargin; constexpr int kVisibleSettingsRowCount = 6; - constexpr int kScrollableSettingsRowCount = 7; const int available_rows_height = std::max(0, settings_content_bottom - first_row_y); const int row_height = std::max(kMinSettingsRowHeight, (available_rows_height - (row_gap * (kVisibleSettingsRowCount - 1))) / @@ -2924,7 +2924,7 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { int rendered = 0; for (int index = app.settings_page_start; index < kScrollableSettingsRowCount && rendered < kVisibleSettingsRowCount; - ++index) { + ++index, ++rendered) { const int row_y = first_row_y + rendered * (row_height + row_gap); switch (index) { case 0: @@ -2986,7 +2986,6 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { default: break; } - rendered++; } app.settings_page_count = rendered; draw_vertical_scrollbar(app, @@ -3661,7 +3660,6 @@ int main(int argc, char* argv[]) { } if (contains_point(skeets_app.settings_scrollbar_down_rect, event.x, event.y)) { - constexpr int kScrollableSettingsRowCount = 7; const int max_start = std::max(0, kScrollableSettingsRowCount - std::max(1, skeets_app.settings_page_count)); if (skeets_app.settings_page_start < max_start) { const int page_step = std::max(1, skeets_app.settings_page_count - 1); @@ -3676,7 +3674,6 @@ int main(int argc, char* argv[]) { } if (contains_point(skeets_app.settings_scrollbar_rect, event.x, event.y)) { - constexpr int kScrollableSettingsRowCount = 7; const int max_start = std::max(0, kScrollableSettingsRowCount - std::max(1, skeets_app.settings_page_count)); if (max_start > 0 && skeets_app.settings_scrollbar_rect.height > 1) { const int relative_y = std::max(0, From cc50ff5fc91deadc2f066521fb249efd359b9d91 Mon Sep 17 00:00:00 2001 From: Evelyn Osman Date: Fri, 13 Mar 2026 13:30:17 +0100 Subject: [PATCH 7/8] Rework calls to follow what koreader is doing --- src/kobo/main.cpp | 61 ++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index d89da7d..fb1c9bd 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -95,9 +95,10 @@ constexpr const char* kEmojiReply = "💬"; constexpr const char* kEmojiRepost = "🌀"; constexpr const char* kButtonSignOut = "Log Out"; constexpr int kDefaultScreenDimTimeoutSeconds = 300; -constexpr int kBacklightPowerOffValue = 0; -// Kobo frontlight sysfs uses bl_power=31 while the light rail is enabled. -constexpr int kBacklightPowerOnValue = 31; +// Default brightness to restore when the sysfs value reads 0 but the light +// was physically on (Nickel sometimes sets the hardware via i2c without +// updating the sysfs brightness register). +constexpr int kFallbackRestoreBrightness = 50; constexpr int kMinSettingsRowHeight = 96; constexpr int kSettingsNoteHeight = 52; constexpr int kSettingsValueChipHeight = 52; @@ -347,14 +348,6 @@ bool write_text_file(const std::string& path, const std::string& value) { return static_cast(file); } -std::string brightness_bl_power_path(const std::string& brightness_path) { - const std::filesystem::path sysfs_path(brightness_path); - if (!sysfs_path.has_parent_path()) { - return {}; - } - return (sysfs_path.parent_path() / "bl_power").string(); -} - std::string detect_frontlight_brightness_path() { if (const char* env_path = std::getenv("SKEETS_FRONTLIGHT_BRIGHTNESS_SYSFS"); env_path && *env_path) { return env_path; @@ -374,22 +367,14 @@ std::string detect_frontlight_brightness_path() { return {}; } +// KOReader's approach for mxc_msp430: only write to the brightness file. +// The brightness range is 0–100. bl_power is a status register that should +// not be written on modern Kobo devices with the mxc_msp430 controller. bool set_frontlight_brightness(const std::string& brightness_path, int brightness) { if (brightness_path.empty()) { return false; } - - const int nonnegative_brightness = std::max(0, brightness); - const std::string bl_power_path = brightness_bl_power_path(brightness_path); - if (path_exists(bl_power_path)) { - if (!write_text_file(bl_power_path, - std::to_string(nonnegative_brightness > 0 ? kBacklightPowerOnValue - : kBacklightPowerOffValue))) { - return false; - } - } - - return write_text_file(brightness_path, std::to_string(nonnegative_brightness)); + return write_text_file(brightness_path, std::to_string(std::max(0, brightness))); } bool dim_frontlight(skeets_app_t& app) { @@ -406,19 +391,28 @@ bool dim_frontlight(skeets_app_t& app) { const auto current_brightness = read_int_file(app.frontlight_brightness_path); if (!current_brightness) { + std::fprintf(stderr, "rewrite app: dim: could not read brightness from %s\n", + app.frontlight_brightness_path.c_str()); app.frontlight_control_available = false; return false; } - if (*current_brightness <= 0) { - return false; - } + + std::fprintf(stderr, "rewrite app: dim: brightness=%d\n", *current_brightness); + + // Save the current brightness so we can restore it later. + // If brightness reads as 0 but the light is physically on (Nickel set it + // via i2c), use a fallback value so we have something visible to restore. + app.saved_frontlight_brightness = (*current_brightness > 0) + ? *current_brightness + : kFallbackRestoreBrightness; if (!set_frontlight_brightness(app.frontlight_brightness_path, 0)) { app.frontlight_control_available = false; return false; } - app.saved_frontlight_brightness = *current_brightness; + std::fprintf(stderr, "rewrite app: dim: wrote brightness=0, will restore to %d\n", + app.saved_frontlight_brightness); return true; } @@ -428,9 +422,16 @@ void restore_frontlight(skeets_app_t& app) { return; } - if (set_frontlight_brightness(app.frontlight_brightness_path, app.saved_frontlight_brightness)) { - app.saved_frontlight_brightness = -1; - } + std::fprintf(stderr, "rewrite app: restore: writing brightness=%d\n", + app.saved_frontlight_brightness); + set_frontlight_brightness(app.frontlight_brightness_path, app.saved_frontlight_brightness); + + // Verify the write. + const auto readback = read_int_file(app.frontlight_brightness_path); + std::fprintf(stderr, "rewrite app: restore: brightness readback=%s\n", + readback ? std::to_string(*readback).c_str() : "failed"); + + app.saved_frontlight_brightness = -1; } void mark_user_activity(skeets_app_t& app) { From 304753c38d3244166eb7e9a2fbb2ae8c59d3ecb1 Mon Sep 17 00:00:00 2001 From: Evelyn Osman Date: Fri, 13 Mar 2026 13:50:41 +0100 Subject: [PATCH 8/8] Brightness control and timeout picker - slightly re-order calls so splash screen always appears --- src/kobo/main.cpp | 256 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 233 insertions(+), 23 deletions(-) diff --git a/src/kobo/main.cpp b/src/kobo/main.cpp index fb1c9bd..6a19313 100644 --- a/src/kobo/main.cpp +++ b/src/kobo/main.cpp @@ -65,7 +65,7 @@ constexpr int kThreadScrollbarWidth = 36; constexpr int kThreadScrollbarGap = 12; constexpr int kThreadScrollbarEndCapHeight = 72; constexpr int kThreadScrollbarEndCapGap = 10; -constexpr int kScrollableSettingsRowCount = 7; +constexpr int kScrollableSettingsRowCount = 8; constexpr int kEmbedImageGap = 6; constexpr int kHeaderLogoMaxWidth = 280; constexpr int kHeaderLogoMaxHeight = 64; @@ -103,6 +103,14 @@ constexpr int kMinSettingsRowHeight = 96; constexpr int kSettingsNoteHeight = 52; constexpr int kSettingsValueChipHeight = 52; +constexpr int kScreenDimTimeoutOptions[] = {0, 10, 30, 60, 300, 600}; +constexpr int kScreenDimTimeoutOptionCount = static_cast(std::size(kScreenDimTimeoutOptions)); +constexpr int kPickerOptionHeight = 72; +constexpr int kPickerPadding = 18; +constexpr std::uint8_t kColorPickerOverlay = 0x40; +constexpr int kSliderTrackHeight = 20; +constexpr int kSliderThumbWidth = 8; + enum class skeets_view_mode_t { dashboard, feed, @@ -247,12 +255,24 @@ struct skeets_app_t { skeets_button_t settings_porn_button; skeets_button_t settings_suggestive_button; skeets_button_t settings_screen_dim_button; + skeets_button_t settings_brightness_button; skeets_button_t settings_diagnostics_button; skeets_button_t settings_sign_out_button; skeets_rect_t settings_scrollbar_up_rect{}; skeets_rect_t settings_scrollbar_down_rect{}; skeets_rect_t settings_scrollbar_rect{}; skeets_rect_t settings_scrollbar_thumb_rect{}; + + // Screen dim picker overlay + bool screen_dim_picker_visible = false; + skeets_rect_t screen_dim_picker_backdrop{}; + skeets_rect_t screen_dim_picker_panel{}; + skeets_rect_t screen_dim_picker_option_rects[6]{}; + + // Brightness slider + int frontlight_brightness = -1; // -1 = use device default on first launch + int launch_brightness = -1; // captured from sysfs at startup + skeets_rect_t brightness_slider_track{}; }; void draw_border(skeets_app_t& app, const skeets_rect_t& rect, std::uint8_t color, int thickness = 2); @@ -292,16 +312,6 @@ std::string format_screen_dim_timeout(int seconds) { return std::to_string(minutes) + " min " + std::to_string(remainder_seconds) + " sec"; } -int next_screen_dim_timeout_seconds(int current) { - constexpr int kTimeoutOptions[] = {0, 30, 60, 300, 600, 1800}; - for (const int candidate : kTimeoutOptions) { - if (candidate > current) { - return candidate; - } - } - return kTimeoutOptions[0]; -} - int sanitize_screen_dim_timeout_seconds(int seconds) { return std::max(0, seconds); } @@ -399,12 +409,15 @@ bool dim_frontlight(skeets_app_t& app) { std::fprintf(stderr, "rewrite app: dim: brightness=%d\n", *current_brightness); - // Save the current brightness so we can restore it later. - // If brightness reads as 0 but the light is physically on (Nickel set it - // via i2c), use a fallback value so we have something visible to restore. - app.saved_frontlight_brightness = (*current_brightness > 0) - ? *current_brightness - : kFallbackRestoreBrightness; + // Use the user's brightness setting if configured, otherwise fall back to + // the current sysfs value (or the fallback constant if sysfs reads 0). + if (app.frontlight_brightness > 0) { + app.saved_frontlight_brightness = app.frontlight_brightness; + } else { + app.saved_frontlight_brightness = (*current_brightness > 0) + ? *current_brightness + : kFallbackRestoreBrightness; + } if (!set_frontlight_brightness(app.frontlight_brightness_path, 0)) { app.frontlight_control_available = false; @@ -1504,6 +1517,7 @@ void load_settings(skeets_app_t& app) { app.allow_suggestive_content = config_get_bool(config, "allow_suggestive_content", false); app.screen_dim_timeout_seconds = sanitize_screen_dim_timeout_seconds( config_get_int(config, "screen_dim_timeout_seconds", kDefaultScreenDimTimeoutSeconds)); + app.frontlight_brightness = config_get_int(config, "frontlight_brightness", -1); config_free(config); } @@ -1520,6 +1534,7 @@ void save_settings(const skeets_app_t& app) { config_set_int(config, "screen_dim_timeout_seconds", sanitize_screen_dim_timeout_seconds(app.screen_dim_timeout_seconds)); + config_set_int(config, "frontlight_brightness", app.frontlight_brightness); config_save(config); config_free(config); } @@ -2875,6 +2890,119 @@ void draw_settings_value_row(skeets_app_t& app, skeets_text_role_t::button); } +void draw_settings_slider_row(skeets_app_t& app, + const skeets_button_t& row, + skeets_rect_t& slider_track_out, + const std::string& label, + const std::string& detail, + int value, + int min_val, + int max_val) { + skeets_framebuffer_fill_rect(app.framebuffer, row.rect, kColorCard); + draw_border(app, row.rect, kColorPostBorder, 2); + + const int text_x = row.rect.x + kCardPadding; + const int text_y = row.rect.y + 12; + draw_text(app, text_x, text_y, label, skeets_text_role_t::settings_label, COLOR_BLACK, kColorCard); + + const int label_bottom = text_y + line_height(skeets_text_role_t::settings_label) + 6; + draw_wrapped_text(app, + text_x, + label_bottom, + row.rect.width - 120, + detail, + skeets_text_role_t::settings_detail, + kColorMeta, + kColorCard); + + // Value label on the right + const std::string val_str = std::to_string(value) + "%"; + const int val_label_x = row.rect.x + row.rect.width - 80; + draw_text(app, val_label_x, text_y, val_str, skeets_text_role_t::settings_label, COLOR_BLACK, kColorCard); + + // Slider track below the description + const int track_x = text_x; + const int track_width = row.rect.width - kCardPadding * 2; + const int track_y = row.rect.y + row.rect.height - kSliderTrackHeight - 14; + + const skeets_rect_t track_rect{track_x, track_y, track_width, kSliderTrackHeight}; + slider_track_out = track_rect; + + // Track background (empty) + skeets_framebuffer_fill_rect(app.framebuffer, track_rect, 0xD0); + draw_border(app, track_rect, kColorPostBorder, 1); + + // Filled portion + const int range = std::max(1, max_val - min_val); + const int clamped = std::clamp(value, min_val, max_val); + const int fill_width = (track_width * (clamped - min_val)) / range; + if (fill_width > 0) { + const skeets_rect_t fill_rect{track_x, track_y, fill_width, kSliderTrackHeight}; + skeets_framebuffer_fill_rect(app.framebuffer, fill_rect, kColorButtonPrimary); + } + + // Thumb marker + const int thumb_x = track_x + fill_width - kSliderThumbWidth / 2; + const skeets_rect_t thumb_rect{std::clamp(thumb_x, track_x, track_x + track_width - kSliderThumbWidth), + track_y - 4, + kSliderThumbWidth, + kSliderTrackHeight + 8}; + skeets_framebuffer_fill_rect(app.framebuffer, thumb_rect, COLOR_BLACK); +} + +void draw_screen_dim_picker_overlay(skeets_app_t& app) { + const int width = app.framebuffer.info.screen_width; + const int height = app.framebuffer.info.screen_height; + + // Semi-transparent backdrop + app.screen_dim_picker_backdrop = {0, 0, width, height}; + skeets_framebuffer_fill_rect(app.framebuffer, app.screen_dim_picker_backdrop, kColorPickerOverlay); + + // Centered panel + const int panel_width = std::min(480, width - kOuterMargin * 4); + const int panel_height = kPickerPadding * 2 + + kScreenDimTimeoutOptionCount * kPickerOptionHeight + + (kScreenDimTimeoutOptionCount - 1) * 4 + + line_height(skeets_text_role_t::settings_label) + 12; + const int panel_x = (width - panel_width) / 2; + const int panel_y = (height - panel_height) / 2; + app.screen_dim_picker_panel = {panel_x, panel_y, panel_width, panel_height}; + + skeets_framebuffer_fill_rect(app.framebuffer, app.screen_dim_picker_panel, COLOR_WHITE); + draw_border(app, app.screen_dim_picker_panel, COLOR_BLACK, 3); + + // Title + const int title_x = panel_x + kPickerPadding; + const int title_y = panel_y + kPickerPadding; + draw_text(app, title_x, title_y, "Screen Dim Timeout", + skeets_text_role_t::settings_label, COLOR_BLACK, COLOR_WHITE); + + // Option rows + const int options_start_y = title_y + line_height(skeets_text_role_t::settings_label) + 12; + const int option_width = panel_width - kPickerPadding * 2; + + for (int i = 0; i < kScreenDimTimeoutOptionCount; ++i) { + const int opt_y = options_start_y + i * (kPickerOptionHeight + 4); + app.screen_dim_picker_option_rects[i] = {title_x, opt_y, option_width, kPickerOptionHeight}; + + const bool selected = (kScreenDimTimeoutOptions[i] == app.screen_dim_timeout_seconds); + const std::uint8_t bg = selected ? kColorButtonPrimary : kColorCard; + const std::uint8_t fg = selected ? COLOR_WHITE : COLOR_BLACK; + + skeets_framebuffer_fill_rect(app.framebuffer, app.screen_dim_picker_option_rects[i], bg); + draw_border(app, app.screen_dim_picker_option_rects[i], COLOR_BLACK, 2); + + const std::string option_text = format_screen_dim_timeout(kScreenDimTimeoutOptions[i]); + draw_centered_text(app, + title_x + option_width / 2, + opt_y + (kPickerOptionHeight - line_height(skeets_text_role_t::button)) / 2, + option_text, + fg, + bg, + skeets_text_role_t::button); + } +} + void render_settings_screen(skeets_app_t& app, bool full_refresh) { const int width = app.framebuffer.info.screen_width; const int height = app.framebuffer.info.screen_height; @@ -2912,6 +3040,8 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { app.settings_suggestive_button.rect = {}; app.settings_diagnostics_button.rect = {}; app.settings_screen_dim_button.rect = {}; + app.settings_brightness_button.rect = {}; + app.brightness_slider_track = {}; app.settings_page_start = clamp_paginated_start(app.settings_page_start, kScrollableSettingsRowCount); app.settings_page_count = 0; @@ -2981,9 +3111,20 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { draw_settings_value_row(app, app.settings_screen_dim_button, "Screen Dim Timeout", - "Tap to cycle the idle timeout before the frontlight turns off until the next input.", + "Tap to choose idle timeout before frontlight turns off.", format_screen_dim_timeout(app.screen_dim_timeout_seconds)); break; + case 7: + app.settings_brightness_button = {{kOuterMargin, row_y, content_width, row_height}, "Brightness"}; + draw_settings_slider_row(app, + app.settings_brightness_button, + app.brightness_slider_track, + "Brightness", + "Tap the slider to adjust the frontlight level.", + std::max(0, app.frontlight_brightness), + 0, + 100); + break; default: break; } @@ -3009,6 +3150,10 @@ void render_settings_screen(skeets_app_t& app, bool full_refresh) { draw_button(app, app.settings_sign_out_button, kColorButtonPrimary); + if (app.screen_dim_picker_visible) { + draw_screen_dim_picker_overlay(app); + } + std::string error_message; const skeets_rect_t screen_rect = full_screen_rect(app); if (!skeets_framebuffer_refresh(app.framebuffer, @@ -3159,6 +3304,9 @@ int main(int argc, char* argv[]) { skeets_app.text_fb.font_h = font_line_height(28, FONT_STYLE_REGULAR); load_brand_assets(skeets_app); + // Show splash immediately so the user sees something while we initialise + render_loading_screen(skeets_app, "Starting", "Initialising input and device state...", true); + if (!skeets_input_open(skeets_app.input, skeets_app.framebuffer.info.screen_width, skeets_app.framebuffer.info.screen_height, @@ -3170,9 +3318,34 @@ int main(int argc, char* argv[]) { return 1; } + render_loading_screen(skeets_app, "Starting", "Detecting frontlight and loading settings...", false); + + // Capture device brightness at launch before loading settings + skeets_app.frontlight_brightness_path = detect_frontlight_brightness_path(); + if (!skeets_app.frontlight_brightness_path.empty()) { + const auto hw_brightness = read_int_file(skeets_app.frontlight_brightness_path); + skeets_app.launch_brightness = hw_brightness.value_or(kFallbackRestoreBrightness); + std::fprintf(stderr, "rewrite app: launch brightness=%d\n", skeets_app.launch_brightness); + } else { + skeets_app.frontlight_control_available = false; + } + + load_settings(skeets_app); + + // Apply brightness: use saved setting if available, otherwise adopt device value + if (skeets_app.frontlight_brightness_path.empty()) { + // no frontlight + } else if (skeets_app.frontlight_brightness < 0) { + skeets_app.frontlight_brightness = skeets_app.launch_brightness; + } else { + set_frontlight_brightness(skeets_app.frontlight_brightness_path, + skeets_app.frontlight_brightness); + } + + render_loading_screen(skeets_app, "Starting", "Checking authentication and network...", false); + refresh_bootstrap_state(skeets_app); refresh_runtime_state(skeets_app); - load_settings(skeets_app); // If auth succeeded, auto-load the feed and switch to feed view if (skeets_app.bootstrap.authenticated) { @@ -3580,7 +3753,32 @@ int main(int argc, char* argv[]) { } if (skeets_app.view_mode == skeets_view_mode_t::settings) { + // Picker overlay takes priority over all other settings touches + if (skeets_app.screen_dim_picker_visible) { + bool option_tapped = false; + for (int i = 0; i < kScreenDimTimeoutOptionCount; ++i) { + if (contains_point(skeets_app.screen_dim_picker_option_rects[i], event.x, event.y)) { + skeets_app.screen_dim_timeout_seconds = kScreenDimTimeoutOptions[i]; + save_settings(skeets_app); + skeets_app.screen_dim_picker_visible = false; + skeets_app.status_line = "Settings updated"; + skeets_app.input_line = "Screen dim timeout set to " + + format_screen_dim_timeout(skeets_app.screen_dim_timeout_seconds); + render_settings_screen(skeets_app, true); + option_tapped = true; + break; + } + } + if (!option_tapped) { + // Tap outside the panel dismisses the picker + skeets_app.screen_dim_picker_visible = false; + render_settings_screen(skeets_app, true); + } + continue; + } + if (contains_point(skeets_app.settings_back_button.rect, event.x, event.y)) { + skeets_app.screen_dim_picker_visible = false; skeets_app.view_mode = skeets_app.settings_return_view; render_active_view(skeets_app, true); continue; @@ -3637,12 +3835,24 @@ int main(int argc, char* argv[]) { } if (contains_point(skeets_app.settings_screen_dim_button.rect, event.x, event.y)) { - skeets_app.screen_dim_timeout_seconds = - next_screen_dim_timeout_seconds(skeets_app.screen_dim_timeout_seconds); + skeets_app.screen_dim_picker_visible = true; + render_settings_screen(skeets_app, true); + continue; + } + + if (contains_point(skeets_app.brightness_slider_track, event.x, event.y)) { + const int track_x = skeets_app.brightness_slider_track.x; + const int track_w = std::max(1, skeets_app.brightness_slider_track.width); + const int relative_x = std::clamp(event.x - track_x, 0, track_w); + skeets_app.frontlight_brightness = (relative_x * 100) / track_w; save_settings(skeets_app); + if (!skeets_app.frontlight_brightness_path.empty()) { + set_frontlight_brightness(skeets_app.frontlight_brightness_path, + skeets_app.frontlight_brightness); + } skeets_app.status_line = "Settings updated"; - skeets_app.input_line = "Screen dim timeout set to " + - format_screen_dim_timeout(skeets_app.screen_dim_timeout_seconds); + skeets_app.input_line = "Brightness set to " + + std::to_string(skeets_app.frontlight_brightness) + "%"; render_settings_screen(skeets_app, true); continue; }