feat(gui): Sorry -- an apology app with a live parametric heart

A new desktop app whose whole point is to say sorry, and to look like
saying it took effort. Nothing in it is a static bitmap.

The heart is the classic parametric heart curve, x = 16sin^3(t) and
y = 13cos(t) - 5cos(2t) - 2cos(3t) - cos(4t), precomputed as a 96-point
integer polygon normalised to a half-width of 1000 and scanline-filled at
whatever size the beat currently has it at. Even-odd crossing counting is
what keeps the notch between the lobes open. The beat is a two-thump
cardiac envelope -- a strong systolic beat and a softer echo -- and the
fill carries a vertical gradient, a soft specular highlight on the upper
lobe, and a six-layer glow. The kernel is built -mno-sse with no floating
point, so every curve, gradient, easing and physics step is integer-only
and needs no trigonometry at runtime.

Around it: three apology letters that type themselves out a character at
a time, and a slow drift of small hearts using the same fill routine.
Answering "I forgive you" brightens the heart and bursts the drift
outward; answering "Not yet" dims it, slows the beat, and starts an
honest count of how long it has been waiting. Neither answer is an exit.

Supporting changes, all of them general rather than app-specific:

  - gui_desktop_request_animation_frame(), so an app can opt into
    full-rate recompositing instead of the ~10 Hz live-content
    heartbeat. It is renewed per paint, so it lapses by itself when the
    app stops animating or its window closes.
  - gui_button_styled() and gui_hitzone() in SUB-WT, letting an app with
    its own palette keep the toolkit's click handling. gui_button_colored
    is now a thin wrapper over the former.
  - A heart glyph in the icon set.

The glow layers skip whatever the heart body is about to cover, which
cuts their blended-pixel cost by roughly 3.5x and pays for the extra
layers that turn contour rings into a smooth halo.

Verified in QEMU on x86_64: all three states driven by real clicks --
hover, forgiveness (brighter heart, burst, live typewriter) and waiting
(dimmed heart, slower beat, running counter). Builds clean on x86_64,
aarch64 and armv8i.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
This commit is contained in:
subhobhai943andClaude Opus 4.8 committed 2026-08-26 18:21:22 +05:30
1 parent 5bd18c4733
commit c9dfa6a1aa
9 files changed
+937 -13

No files matched your search

+17 -3
View File
@@ -64,14 +64,16 @@ static const desktop_app_t g_apps[] = {
{ "Clock", GUI_ICON_CLOCK, gui_app_clock_launch, 460, 120, 280, 260 },
{ "Settings", GUI_ICON_SETTINGS, gui_app_settings_launch, 160, 60, 400, 320 },
{ "Analytics", GUI_ICON_MONITOR, gui_app_analytics_launch,320, 70, 560, 400 },
{ "Sorry", GUI_ICON_HEART, gui_app_sorry_launch, 260, 60, 660, 470 },
{ "About", GUI_ICON_INFO, gui_app_about_launch, 240, 140, 330, 170 },
};
#define APP_COUNT ((int)(sizeof(g_apps) / sizeof(g_apps[0])))
// Which of the above appear on the desktop itself.
// Indices into g_apps[]: Terminal, Files, Monitor, Kernel Log, KTest, Life,
// Rust Lab, Settings, Analytics. (Objects/RegEdit live in the start menu.)
static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15 };
// Rust Lab, Settings, Analytics, Sorry. (Objects/RegEdit live in the start
// menu.)
static const int g_desktop_icons[] = { 0, 1, 2, 4, 5, 9, 10, 14, 15, 16 };
#define DESKTOP_ICON_COUNT ((int)(sizeof(g_desktop_icons) / sizeof(g_desktop_icons[0])))
static bool g_start_menu_open = false;
@@ -89,6 +91,7 @@ static uint64_t g_fps_last_ms = 0;
static uint64_t g_fps_last_frames = 0;
static uint64_t g_last_scene_ms = 0; // last full scene recomposite (for heartbeat)
static int g_prev_mx = -1, g_prev_my = -1;
static bool g_anim_request = false; // an app asked for the next frame back
// Target frame budget: 60 FPS == 16666 us. The compositor only repaints the
// full scene when it is dirty; between times it just moves the cursor overlay,
@@ -536,6 +539,10 @@ void gui_desktop_close_menus(void) {
g_context_menu_open = false;
}
void gui_desktop_request_animation_frame(void) {
g_anim_request = true;
}
void gui_desktop_request_exit(void) { g_desktop_running = false; }
static void exit_confirm_cb(gui_dialog_result_t result, void* ctx) {
@@ -807,9 +814,16 @@ int gui_desktop_run(void) {
g_start_menu_open || g_context_menu_open ||
gui_dialog_is_open();
// Claim any animation request left by the previous frame's painters.
// Clearing it here means a request only survives while an app keeps
// renewing it from its paint routine.
bool animating = g_anim_request;
g_anim_request = false;
// A full recomposite is needed when anything but the bare cursor moved;
// otherwise we keep the retained scene and just slide the cursor overlay.
bool scene_dirty = wm_animating || interacting || heartbeat || (g_frames == 0);
bool scene_dirty = wm_animating || interacting || heartbeat || animating ||
(g_frames == 0);
if ((g_frames & 0x1F) == 0) sample_cpu_history();