feat: split desktop implementation into multiple source files, reorganize apps
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* app_mandelbrot.cpp
|
||||
* MontaukOS Desktop - Mandelbrot set visualizer
|
||||
* Supports zoom (scroll wheel), pan (drag), and reset (R key)
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "apps_common.hpp"
|
||||
|
||||
// ============================================================================
|
||||
// Fixed-point Mandelbrot (avoids FPU/SSE dependency issues)
|
||||
// Uses 28.36 fixed-point: 36 fractional bits gives ~1e-10 precision
|
||||
// ============================================================================
|
||||
|
||||
using fp_t = int64_t;
|
||||
static constexpr int FP_SHIFT = 36;
|
||||
static constexpr fp_t FP_ONE = (fp_t)1 << FP_SHIFT;
|
||||
|
||||
static inline fp_t fp_from_int(int v) { return (fp_t)v << FP_SHIFT; }
|
||||
|
||||
static inline fp_t fp_mul(fp_t a, fp_t b) {
|
||||
// Split to avoid overflow: a * b >> SHIFT
|
||||
// Use 128-bit intermediate via compiler builtin
|
||||
__int128 r = (__int128)a * b;
|
||||
return (fp_t)(r >> FP_SHIFT);
|
||||
}
|
||||
|
||||
// fp_div for small numerators (avoids __divti3 by using 64-bit division)
|
||||
// Only valid when a is small enough that a << FP_SHIFT fits in int64_t
|
||||
static inline fp_t fp_div_small(int a, int b) {
|
||||
return ((fp_t)a << FP_SHIFT) / (fp_t)b;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
|
||||
static constexpr int MB_MAX_ITER = 256;
|
||||
static constexpr int MB_TOOLBAR_H = 32;
|
||||
|
||||
struct MandelbrotState {
|
||||
DesktopState* desktop;
|
||||
fp_t center_x, center_y; // center of view in fractal coords
|
||||
fp_t scale; // units per pixel (fixed-point)
|
||||
int max_iter;
|
||||
bool needs_render;
|
||||
// Drag state
|
||||
bool dragging;
|
||||
int drag_start_x, drag_start_y;
|
||||
fp_t drag_start_cx, drag_start_cy;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Color palette
|
||||
// ============================================================================
|
||||
|
||||
static uint32_t mandelbrot_color(int iter, int max_iter) {
|
||||
if (iter >= max_iter) return 0xFF000000; // black for inside set
|
||||
|
||||
// Smooth cycling palette using bit manipulation
|
||||
int t = (iter * 7) & 0xFF;
|
||||
int phase = (iter * 7) >> 8;
|
||||
|
||||
uint8_t r, g, b;
|
||||
switch (phase % 6) {
|
||||
case 0: r = 255; g = (uint8_t)t; b = 0; break;
|
||||
case 1: r = (uint8_t)(255 - t); g = 255; b = 0; break;
|
||||
case 2: r = 0; g = 255; b = (uint8_t)t; break;
|
||||
case 3: r = 0; g = (uint8_t)(255 - t); b = 255; break;
|
||||
case 4: r = (uint8_t)t; g = 0; b = 255; break;
|
||||
default: r = 255; g = 0; b = (uint8_t)(255 - t); break;
|
||||
}
|
||||
|
||||
return 0xFF000000 | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Render
|
||||
// ============================================================================
|
||||
|
||||
static void mandelbrot_render(MandelbrotState* mb, uint32_t* pixels, int w, int h) {
|
||||
int render_h = h - MB_TOOLBAR_H;
|
||||
if (render_h <= 0) return;
|
||||
|
||||
fp_t half_w = fp_mul(fp_from_int(w / 2), mb->scale);
|
||||
fp_t half_h = fp_mul(fp_from_int(render_h / 2), mb->scale);
|
||||
fp_t x_min = mb->center_x - half_w;
|
||||
fp_t y_min = mb->center_y - half_h;
|
||||
fp_t bailout = fp_from_int(4);
|
||||
|
||||
for (int py = 0; py < render_h; py++) {
|
||||
fp_t ci = y_min + fp_mul(fp_from_int(py), mb->scale);
|
||||
uint32_t* row = pixels + (py + MB_TOOLBAR_H) * w;
|
||||
|
||||
for (int px = 0; px < w; px++) {
|
||||
fp_t cr = x_min + fp_mul(fp_from_int(px), mb->scale);
|
||||
|
||||
fp_t zr = 0, zi = 0;
|
||||
int iter = 0;
|
||||
|
||||
while (iter < mb->max_iter) {
|
||||
fp_t zr2 = fp_mul(zr, zr);
|
||||
fp_t zi2 = fp_mul(zi, zi);
|
||||
if (zr2 + zi2 > bailout) break;
|
||||
fp_t new_zr = zr2 - zi2 + cr;
|
||||
zi = fp_mul(fp_from_int(2), fp_mul(zr, zi)) + ci;
|
||||
zr = new_zr;
|
||||
iter++;
|
||||
}
|
||||
|
||||
row[px] = mandelbrot_color(iter, mb->max_iter);
|
||||
}
|
||||
}
|
||||
|
||||
mb->needs_render = false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
static void mb_on_draw(Window* win, Framebuffer& fb) {
|
||||
MandelbrotState* mb = (MandelbrotState*)win->app_data;
|
||||
if (!mb) return;
|
||||
|
||||
Canvas c(win);
|
||||
|
||||
if (mb->needs_render) {
|
||||
mandelbrot_render(mb, win->content, c.w, c.h);
|
||||
}
|
||||
|
||||
// Draw toolbar over the render
|
||||
c.fill_rect(0, 0, c.w, MB_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5));
|
||||
c.hline(0, MB_TOOLBAR_H - 1, c.w, colors::BORDER);
|
||||
|
||||
// Reset button
|
||||
c.button(8, 4, 60, 24, "Reset", colors::ACCENT, colors::WHITE, 4);
|
||||
|
||||
// Iter +/- buttons
|
||||
char iter_str[24];
|
||||
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
|
||||
int fh = system_font_height();
|
||||
c.text(80, (MB_TOOLBAR_H - fh) / 2, iter_str, colors::TEXT_COLOR);
|
||||
|
||||
int iter_text_w = text_width(iter_str);
|
||||
int btn_x = 80 + iter_text_w + 8;
|
||||
c.button(btn_x, 4, 24, 24, "-", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
|
||||
c.button(btn_x + 28, 4, 24, 24, "+", Color::from_rgb(0xAA, 0xAA, 0xAA), colors::WHITE, 4);
|
||||
|
||||
// Hint
|
||||
const char* hint = "Scroll=zoom Drag=pan";
|
||||
int hw = text_width(hint);
|
||||
c.text(c.w - hw - 8, (MB_TOOLBAR_H - fh) / 2, hint, Color::from_rgb(0x99, 0x99, 0x99));
|
||||
}
|
||||
|
||||
static void mb_on_mouse(Window* win, MouseEvent& ev) {
|
||||
MandelbrotState* mb = (MandelbrotState*)win->app_data;
|
||||
if (!mb) return;
|
||||
|
||||
Rect cr = win->content_rect();
|
||||
int lx = ev.x - cr.x;
|
||||
int ly = ev.y - cr.y;
|
||||
|
||||
// Handle toolbar clicks
|
||||
if (ev.left_pressed() && ly < MB_TOOLBAR_H) {
|
||||
// Reset button
|
||||
Rect reset_r = {8, 4, 60, 24};
|
||||
if (reset_r.contains(lx, ly)) {
|
||||
mb->center_x = -fp_from_int(1) / 2; // -0.5
|
||||
mb->center_y = 0;
|
||||
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
|
||||
mb->max_iter = MB_MAX_ITER;
|
||||
mb->needs_render = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Iter buttons
|
||||
char iter_str[24];
|
||||
snprintf(iter_str, sizeof(iter_str), "Iter: %d", mb->max_iter);
|
||||
int iter_text_w = text_width(iter_str);
|
||||
int btn_x = 80 + iter_text_w + 8;
|
||||
|
||||
Rect minus_r = {btn_x, 4, 24, 24};
|
||||
Rect plus_r = {btn_x + 28, 4, 24, 24};
|
||||
|
||||
if (minus_r.contains(lx, ly)) {
|
||||
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
|
||||
return;
|
||||
}
|
||||
if (plus_r.contains(lx, ly)) {
|
||||
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Drag panning
|
||||
if (ev.left_pressed() && ly >= MB_TOOLBAR_H) {
|
||||
mb->dragging = true;
|
||||
mb->drag_start_x = lx;
|
||||
mb->drag_start_y = ly;
|
||||
mb->drag_start_cx = mb->center_x;
|
||||
mb->drag_start_cy = mb->center_y;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb->dragging && ev.left_held()) {
|
||||
int dx = lx - mb->drag_start_x;
|
||||
int dy = ly - mb->drag_start_y;
|
||||
mb->center_x = mb->drag_start_cx - fp_mul(fp_from_int(dx), mb->scale);
|
||||
mb->center_y = mb->drag_start_cy - fp_mul(fp_from_int(dy), mb->scale);
|
||||
mb->needs_render = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mb->dragging && !ev.left_held()) {
|
||||
mb->dragging = false;
|
||||
}
|
||||
|
||||
// Scroll zoom (centered on mouse position)
|
||||
if (ev.scroll != 0 && ly >= MB_TOOLBAR_H) {
|
||||
int render_h = cr.h - MB_TOOLBAR_H;
|
||||
// Mouse position in fractal coords before zoom
|
||||
fp_t mx_frac = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
|
||||
fp_t my_frac = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
|
||||
|
||||
if (ev.scroll < 0) {
|
||||
// Zoom in
|
||||
mb->scale = fp_mul(mb->scale, fp_from_int(3) / 4); // * 0.75
|
||||
} else {
|
||||
// Zoom out
|
||||
mb->scale = fp_mul(mb->scale, fp_from_int(4) / 3); // * 1.33
|
||||
}
|
||||
|
||||
// Adjust center so mouse stays at same fractal point
|
||||
fp_t new_mx = mb->center_x + fp_mul(fp_from_int(lx - cr.w / 2), mb->scale);
|
||||
fp_t new_my = mb->center_y + fp_mul(fp_from_int((ly - MB_TOOLBAR_H) - render_h / 2), mb->scale);
|
||||
mb->center_x += mx_frac - new_mx;
|
||||
mb->center_y += my_frac - new_my;
|
||||
|
||||
mb->needs_render = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void mb_on_key(Window* win, const Montauk::KeyEvent& key) {
|
||||
MandelbrotState* mb = (MandelbrotState*)win->app_data;
|
||||
if (!mb || !key.pressed) return;
|
||||
|
||||
if (key.ascii == 'r' || key.ascii == 'R') {
|
||||
Rect cr = win->content_rect();
|
||||
mb->center_x = -fp_from_int(1) / 2;
|
||||
mb->center_y = 0;
|
||||
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
|
||||
mb->max_iter = MB_MAX_ITER;
|
||||
mb->needs_render = true;
|
||||
} else if (key.ascii == '+' || key.ascii == '=') {
|
||||
if (mb->max_iter < 4096) { mb->max_iter *= 2; mb->needs_render = true; }
|
||||
} else if (key.ascii == '-') {
|
||||
if (mb->max_iter > 32) { mb->max_iter /= 2; mb->needs_render = true; }
|
||||
}
|
||||
}
|
||||
|
||||
static void mb_on_close(Window* win) {
|
||||
if (win->app_data) {
|
||||
montauk::mfree(win->app_data);
|
||||
win->app_data = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Mandelbrot launcher
|
||||
// ============================================================================
|
||||
|
||||
void open_mandelbrot(DesktopState* ds) {
|
||||
int idx = desktop_create_window(ds, "Mandelbrot", 120, 60, 500, 400);
|
||||
if (idx < 0) return;
|
||||
|
||||
Window* win = &ds->windows[idx];
|
||||
Rect cr = win->content_rect();
|
||||
|
||||
MandelbrotState* mb = (MandelbrotState*)montauk::malloc(sizeof(MandelbrotState));
|
||||
montauk::memset(mb, 0, sizeof(MandelbrotState));
|
||||
mb->desktop = ds;
|
||||
mb->center_x = -fp_from_int(1) / 2; // -0.5
|
||||
mb->center_y = 0;
|
||||
mb->scale = fp_div_small(3, cr.w > 0 ? cr.w : 400);
|
||||
mb->max_iter = MB_MAX_ITER;
|
||||
mb->needs_render = true;
|
||||
mb->dragging = false;
|
||||
|
||||
win->app_data = mb;
|
||||
win->on_draw = mb_on_draw;
|
||||
win->on_mouse = mb_on_mouse;
|
||||
win->on_key = mb_on_key;
|
||||
win->on_close = mb_on_close;
|
||||
}
|
||||
Reference in New Issue
Block a user